Mailing List Archive


[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: [tlug] ruby and python in Japan



On Mon, 26 Feb 2007, Stephen J. Turnbull wrote:

What's the use-case for changing a class on the fly?

Sorry about taking so long to generate a reply on this.

Most of the talk about changing classes on the fly has been related to
monkey-patching, but that's not, in my experience, the common use case
for changing classes at run-time in Ruby. It's far more often used for a
form of meta-programming.

I should first make it clear that changing class definitions at run-time
is actually the only form of class definition in Ruby: when you define
a class, the statements in that definition are executed at runtime. So,
for example, this will print out Hello to the standard output:

    class Foo
	puts("Hello.")
    end

The 'class Foo' bit defines (if it's not already defined) a constant
Foo, creates a new object of class Class, assigns it to that constant,
and then leaves you (until you get to the 'end' statement) in a context
where statements and expressions are evaluated in the context of that
class:

    class Bar
	puts("Self class: #{self.class} Self.inspect: #{inspect}")
	puts("Contains baz: #{instance_methods.include?('baz')}")
	def baz; "abc"; end
	puts("Contains baz: #{instance_methods.include?('baz')}")
    end

produces:

    self class: Class  self.inspect: Bar
    Contains baz: false
    Contains baz: true

(Module#instance_methods [Module is the superclass of Class] returns
a list of the methods defined in that module. Don't confuse it with
Object#methods, which returns the list of methods that that particular
object responds to.)

So this is really just syntatic sugar for

    Bar = Class.new
    Bar.send(:define_method, :baz, lambda { "abc" })
    puts(Bar.new.baz)

which produces

    abc

(I use send to call Bar#define_method because it's a private method.)

A very frequent use of this is the attr_* methods, which are actually
just functions that module responds to, which create new methods in the
module in which they're invoked:

    class Quux
	puts(instance_methods.select { |m| /^xy/.match(m) }.inspect)
	attr_accessor :xyzzy
	puts(instance_methods.select { |m| /^xy/.match(m) }.inspect)
    end

produces:

    []
    ["xyzzy", "xyzzy="]

I've used this technique to create a DSL for SWF parsing in Starling's
RSWF library. Here are some incomplete fragments to demonstrate:

    class SWFData

	def self.swfio(io_object)
	    ...
	    self.const_set(:IO_OBJECTS, []) unless self.const_defined?(:IO_OBJECTS)
	    self::IO_OBJECTS << io_object
	    io_object.setup_data_class(self)
	end

	...

    end

    class RGBA
	...
    end

    class RGBA_IO

	def setup_data_class(data_class)
	    data_class.instance_eval { attr_accessor :color }
	end

	def clear_data(data)
	    data.color = RGBA.new(0xffffff, 0xff)
	end

	def read_data(data, stream)
	    data.color = RGBA.new(stream.read_ubyte << 16 |
		stream.read_ubyte << 8 | stream.read_ubyte,
		stream.read_ubyte)
	    stream.log { ' rgba=' + data.color.inspect }
	end

	...

    end

    class BackgroundColor < Element
	tag_number 9
	swfio Struct::RGB_IO.new
    end

What's happening here is that, when we define BackgroundColor, we call
swfio with an object that knows how to do IO for the color component.
(This is a very simple example; typically there would be a series
of swfio statements for the various components of a tag.) The swfio
function adds an IO_OBJECTS constant to the BackgroundColor class, sets
it to an empty array, and appends the given IO object to that array. It
then asks the IO object to further define the class, and the RGBA_IO
creates 'color' and 'color=' methods on the BackgroundColor class.
Later, when given an instance of the BackgroundColor class, it uses
those methods to store the data that it reads and retrieve the data that
it writes, which would be in the form of an RGBA object. (For many of
the simpler fields in a tag, this would be just a number or a String.)

The class definition done by an IO object gets more complex. Some IO
objects take parameters to determine the name of the accessors:

    class DefineEditText < Definition
	tag_number 37
	swfio Struct::ID_IO.new
	swfio Struct::Rectangle_IO.new(:bounds)
	swfio Struct::Flags_IO.new(
	    :has_text, :word_wrap, :multiline, :password, :readonly,
	    :has_color, :has_max_length, :has_font, :reserved1,
	    :auto_size, :has_layout, :no_select, :border, :reserved2,
	    :html, :use_outlines)
	swfio Struct::RefID_IO.new
	swfio Struct::UShort_IO.new(:font_height)
	swfio Struct::RGBA_IO.new
	swfio Struct::UShort_IO.new(:max_length)
	...
	swfio Struct::String_IO.new(:variable_name)
	swfio Struct::String_IO.new(:initial_text)
	...
    end

Others will define a number of new special-purpose methods on the class
being created:

    module JPEGOperations
	# Various method definitons here
    end

    class JPEGSegments_IO < List_IO
	...
	def setup_data_class(data_class)
	    super
	    data_class.instance_eval { include JPEGOperations }
	end
	...
    end

I hope that this isn't too confusing. I don't think it's exactly the
clearest and easiest way anybody's ever come up with to do this sort of
thing, myself, which is one of the reasons I'm tending away from Ruby
these days.

cjs
--
Curt Sampson       <cjs@??>        +81 90 7737 2974


Home | Main Index | Thread Index

Home Page Mailing List Linux and Japan TLUG Members Links