hoodwink.d enhanced
RSS
2.0
XHTML
1.0

RedHanded

One Small Flask of Metaprogramming Elixir #

by why in inspect

We could make metaprogramming easier for the common streetsfolk. I’m not giving a triumphant answer here. I’m just wondering what else you knobbly ducks out there have done like this.

So you have this class which defines a structure for scripts within a larger program (MouseHole in this case):

 class UserScript
   attr_accessor :title, :description, :version, :mount,
                 :register_uri, :rewrite
   def initialize
     @register_uri, @rewrite = [], {}
   end
 end

Nothing special. But the initialize does tell us that register_uri and rewrite are an array and a hash, respectively.

Use a method_missing to set the accessors based on their defaults in initialize:

 meta_make(UserScript) do
   title "MouseCommand" 
   description "A-wiki-and-a-shell-in-one." 
   version "2.0" 

   mount :cmd do
     %{<html> .. </html>}
   end

   rewrite HTML, XML do
     document.change_someways!
   end

   rewrite CSS do
     document.gsub! '.title', '.titleSUPERIOR'
   end

   # allow methods to be defined
   def to_gravy; end
 end

So, since rewrite was set up in initialize as a hash, the definition above will result in:

 @title = "MouseCommand" 
 @description = "A-wiki-and-a-shell-in-one." 
 @version = "2.0" 
 @mount = [:cmd, #<Proc:903e>],
 @rewrite = {
   HTML => #<Proc:0xf140>,
   XML => #<Proc:0xf140>,
   CSS => #<Proc:0x34a3>
 }

It’s very Railsish, but as people are now so accustomed to that syntax, it could ease transition of Railsers to your project, whatever it be.

Here’s the meta_make method:

 def meta_make(klass, &blk)
   o = klass.new
   (class << o; self; end).instance_eval { @__obj = o }
   class << o
     def self.method_missing(m, *args, &blk)
       set_attr(m, *args, &blk)
     end
     def self.set_attr(m, *args, &blk)
       if blk
         args << nil if args.empty?
         args << blk 
       end
       v = @__obj.instance_variable_get("@#{m}")
       if v.respond_to? :to_ary
         (v = v.to_ary).push args
       elsif v.respond_to? :to_hash
         v = v.to_hash
         while args.length > 1
           v[args.unshift] = args.last
         end
       elsif args.length <= 1
         v = args.first
       else
         v = args
       end
       @__obj.instance_variable_set("@#{m}", v)
     end
   end
   (class << o; self; end).class_eval &blk
   o
 end
said on 29 Nov 2005 at 02:39

Brilliant. (Sorry, I am all out of zany & wacky.)

said on 29 Nov 2005 at 02:44

Very cool :)

said on 29 Nov 2005 at 03:15

I… have no idea what to add.

Quick question though: Any reason you decided to change up between class_eval and instance_eval when calling against the eigenclass?

said on 29 Nov 2005 at 07:37

Hmmm, it looks like @register_uri in the UserScript class gets changed to @mount in method_missing… typo?

said on 29 Nov 2005 at 07:39

mh.. is’nt there something like this in the stdlib ? IIRC Tk does named arguments this way in ruby, since we can’t mimic the tcl syntax better

said on 29 Nov 2005 at 10:42

i’m now entirely convinced that _why is a ruby wizzard sent from the future to save mankind..

said on 29 Nov 2005 at 13:12

It makes my head hurt. Can anyone explain in really simple words what this is about.

said on 29 Nov 2005 at 14:34

Using method_missing is a good way to create difficult to debug code. It’s a last resort, not a first resort.

Use “yield self if block_given?” as one of the first things in initialize, then just use ”||=” within initialize to set things that need setting. Easier to read. Faster, too, I’ll bet.

said on 29 Nov 2005 at 15:40

aberant: yes, it is about time we developed a mythology around him.

ouch: here’s the basics:

meta_make(UserScript) creates an instance of UserScript and execs the attach’d block in the context of its eigenclass (sort of as if it were class << UserScript.new).

The key is that it slips some additional bits into the eigenclass before doing so. That call to rewrite XML, HTML, for example? It hits the eigenclass’s method_missing, which has been made basically just a call to set_attr.

set_attr looks at the name of the attempted method call (:rewrite), yanks out @rewrite from the object (thoughtfully made available via __obj), sees it’s a hash, and merges in its parameters accordingly (each arg becomes a key, the block becomes the value).

We’ve got different rules for instance variables which are arrays and so forth, but that’s the basic idea. See the definition of set_attr above for the specifics.

said on 29 Nov 2005 at 15:45

Daniel Berger: It is true that much of this could (should, perhaps) be done in initialize, but yield self is quite different to instance_exec’ing the block on the eigenclass.

Perhaps, though, that makes more sense in the context of MouseHole, where UserScript is sort of a factory for one-off singletons rather than a class being used the normal way.

For most purposes you could probably squish this down into less meta-ness.

said on 29 Nov 2005 at 15:50

Hmm, and it is very meta, isn’t it? We’re adding things to the eigenclass’s own eigenclass here.


   class << o ; class << self
     def set_attr(m, *args, &blk)
       if blk
         args << nil if args.empty?
         args << blk 
       end
       v = @__obj.instance_variable_get("@#{m}")
       if v.respond_to? :to_ary
         (v = v.to_ary).push args
       elsif v.respond_to? :to_hash
         v = v.to_hash
         while args.length > 1
           v[args.unshift] = args.last
         end
       elsif args.length <= 1
         v = args.first
       else
         v = args
       end
       @__obj.instance_variable_set("@#{m}", v)
     end
     alias method_missing set_attr
   end end
said on 29 Nov 2005 at 16:26
MenTaLguY, what does that code do that this doesn’t?

class UserScript
   attr_accessor :title, :description, :version,
   attr_accessor :mount, :register_uri, :rewrite
   def initialize
      yield self if block_given?

      @title       ||= "MouseCommand" 
      @description ||= "A-wiki-and-a-shell-in-one." 
      @version     ||= "2.0" 
      @mount       ||= [:cmd, lambda{ %{<html> .. </html>}]

      @rewrite["HTML"] ||= document.change_someways!
      @rewrite["XML"]  ||= document.change_someways!
      @rewrite["CSS"]  ||=
         document.gsub!('.title','.titleSUPERIOR')
   end
end

said on 29 Nov 2005 at 16:28

This gives me an idea. We can secret our instance variables in in our eigenclass, can’t we?

Maybe that is useful for when we are writing something meta and want to be all “hands off my instance variables”.



class Class
  def eigen_reader( *names )
    names.each do |name|
      ivar = "@#{ name }" 
      define_method( name ) do
        (class << self ; self ; end).
          instance_variable_get ivar
      end
    end
  end
  def eigen_writer( *names )
    names.each do |name|
      ivar = "@#{ name }" 
      define_method( "#{ name }=" ) do |value|
        (class << self ; self ; end).
          instance_variable_set ivar, value
      end
    end
  end
  def eigen_accessor( *names )
    eigen_reader *names
    eigen_writer *names
  end
end
said on 29 Nov 2005 at 16:32

Daniel: with make_meta that stuff is added to an instance, not to the class.

said on 29 Nov 2005 at 16:34

make_meta? meta_make, rather. The meta method, for mouseHole.

said on 29 Nov 2005 at 16:37

Actually… if we’re putting stuff in eigenland, why use instance variables a’tall?


class Class
  def magic_accessor( *names )
    names.each do |name|
      define_method( name ) { nil }
      define_method( "#{ name }=" ) do |value|
        (class << self ; self ; end).class_eval do
          define_method( name ) { value }
        end
      end
    end
  end
end
said on 29 Nov 2005 at 16:48

Hey, since we can have eigenclasses of eigenclasses, we can do this:


class Object
  def meta( n=1 )
    meta = self
   n.times do
      meta = (class << meta ; self ; end)
    end
    meta
 end
end

How meta can you go?

Object.new.meta(5)

=> #<Class:#<Class:#<Class:#<Class:#<Class:#<Object:0x3247b10>>>>>>

said on 29 Nov 2005 at 16:59

To get around the method_missing / debug problem, I think I’d raise a noodle if there’s no setter.

 raise ProblemChild unless respond_to? "#{m}=" 

In set_attr. Dan, we’re trying to make simple scripts pretty for beginners, not reach some moral/ethical crossroad.

said on 29 Nov 2005 at 17:10

Okay soo… to bring this back on topic, here’s what things might look like doing in initialize. Let’s give ourselves a meta_eval first, though.


class Object
  def meta_eval( n=1, &blk )
   meta( n ).instance_eval &blk
  end
end

So, then…


class MetaMaid
  eigen_accessor :__obj

  def initialize( &blk )
    self.__obj = self
    meta_eval( 2 ) do
      def set_attr( m, *args, &blk )
        # same as _why had
      end
      alias set_attr method_missing
    end
    meta_eval &blk
  end
end

Then I think you could do like:

 class UserScript < MetaMaid
   attr_accessor :title, :description,
                 :version, :mount,
                 :register_uri, :rewrite
   def initialize( &blk )
     @register_uri, @rewrite = [], {}
     super( &blk )
   end
 end

And then turn out your scripts like:


UserScript.new do
  # also same as _why had
end

I don’t know. You could probably make MetaMaid a module even.

said on 29 Nov 2005 at 17:14

Er, the “back on topic” line was aimed at myself, since I’ve been astronauting the eigenspace when we should instead be looking making things friendly for the newbies.

Meta-newbies might like this Object#meta_eval thing though.

said on 29 Nov 2005 at 17:51

I’m trying to wrap my head around this code, but coming up a bit short. Some of the syntax is chewy. And heck, I’m new to this ruby thing anyhoo. So…

What is going on with @__obj and @#{m} ? Meaning mostly the syntax.

And in…


 meta_make(UserScript) do
   title "MouseCommand" 
   description "A-wiki-and-a-shell-in-one." 
   version "2.0" 

I understand that “MouseCommand” is getting assigned to title, but why? how?

Sorry if my questions are a bit simpleton, but I feel I am on there verge of understanding the concepts, but I don’t quite have the syntax/idiom down. Any help would be greatfully appreciated.

-cheers

said on 29 Nov 2005 at 18:07

Syntactically, @__obj is an ordinary instance variable. No magic in itself.

"@#{m}" is basically the same as "@" + m.to_s
said on 29 Nov 2005 at 18:10

Also, I have been subconsciously ripping off why’s metaid. Wondered why the name MetaMaid seemed so natural. Object#meta_eval indeed. My only contribution is the multi-level recursion and maybe the eigen_accessor business.

said on 29 Nov 2005 at 18:42

Ok, then why the double underscore? That spurs me to find meaning there. If this is essentially a primer for nubies to understand the concepts of metaprogramming, then I have a few observations/questions. Most of the Ruby code I have seen is very human readable with little short cuts in naming and when illustrating concepts syntactical shortcuts are for the most part left out. So, I might rename some of your variables such as o to eigen_object or v to eigen_instance or m to eigen_setter. I like the code, it tickles my thinking parts, especially those that are in love with self reference and recursion. It’s really exciting and I want to wrap my noggin around as soon as possible so I might add to the dialog…

said on 29 Nov 2005 at 18:45

Oh MenTaLguY, and at some point in my mental process I apparently reascribed authorship of the code to you. Bad pointer arithmatic will get you every time.

said on 29 Nov 2005 at 18:58

Adharma: This isn’t very good code for teaching beginners how write metaprogramming mysteries. It is a bit of code to make use of, though. If you understand the first three bits of code in the post above, then you can probably use the fourth without totally digesting it.

The meta_make code is guts code. It looks like guts and it is.

The double-underscore is just stupid. To prevent clash. I’m actually hoping me or anyone will clean up the stuff. I should start working on getting this post off the front page.

said on 29 Nov 2005 at 22:23

_why, your code is showing.

unshift inside of the while loop in meta_make will never cause the array to shrink. Simple typo.

As for avoiding method_missing, I think I have a solution (it uses a string eval though, so I’m like “Blaaaah”)


class Module      
  def advanced_accessor(*args)
    attr_accessor *args
    @__nuts_for_later = args
  end
end

def meta_make(klass, &blk)
  retrieved_nuts = klass.instance_variable_get(:@__nuts_for_later)
  o = klass.new

  (class << o; self; end).instance_eval { @__obj = o }
  (class << o; self; end).instance_eval { @__retrieved_nuts = retrieved_nuts }

  class << o
    @__retrieved_nuts.each do |sym|
      var_sym = (":@" + sym.to_s)
      new_method = <<DONE
def self.#{sym.to_s} (*args, &blk)
  if blk
    args << nil if args.empty?
    args << blk 
  end
  v = @__obj.instance_variable_get(#{var_sym})
  if v.respond_to? :to_ary
    (v = v.to_ary).push args
  elsif v.respond_to? :to_hash
    v = v.to_hash
    while args.length > 1
      v[args.shift] = args.last
    end
  elsif args.length <= 1
    v = args.first
  else
    v = args
  end
  @__obj.instance_variable_set(#{var_sym}, v)
end
DONE
      class_eval(new_method)
    end
  end

  (class << o; self; end).instance_eval &blk

  o
end

 class UserScript
   advanced_accessor :title, :description, :version, :mount,
                 :register_uri, :rewrite
   def initialize
     @register_uri, @rewrite = [], {}
   end
 end

 meta_make(UserScript) do
   title "MouseCommand" 
   description "A-wiki-and-a-shell-in-one." 
   version "2.0" 

   mount :cmd do
     %{<html> .. </html>}
   end

   rewrite "HTML", "XML" do
     document.change_someways!
   end

   rewrite "CSS" do
    document.gsub! '.title', '.titleSUPERIOR'
   end

   # allow methods to be defined
   def to_gravy; end
 end

said on 30 Nov 2005 at 12:28
Adharma: I changed the variable names in _why’s code, see if this helps you grok it better?
def meta_make(klass, &blk)

   some_class = klass.new
   (class << some_class; self; end).instance_eval { @whys_temp_some_class = some_class }
   class << some_class
     # Gets called when there is no method of 'name'
     def self.method_missing(name, *args, &blk)
       set_attr(name, *args, &blk)
     end

     def self.set_attr(method_name, *args, &blk)
       if blk
         args << nil if args.empty?
         args << blk 
       end

       our_new_var = @whys_temp_some_class.instance_variable_get("@#{method_name}")

       if our_new_var.respond_to? :to_ary
         (our_new_var = our_new_var.to_ary).push args
       elsif our_new_var.respond_to? :to_hash
         our_new_var = our_new_var.to_hash
         while args.length > 1
           our_new_var[args.unshift] = args.last
         end
       elsif args.length <= 1
         our_new_var = args.first
       else
         our_new_var = args
       end

       @whys_temp_some_class.instance_variable_set("@#{method_name}", our_new_var)
     end
   end

   (class << some_class; self; end).class_eval &blk
   some_class
 end
said on 01 Dec 2005 at 15:15

Ruby beginners should look here: pleac.sf.net or here: www.rubyquiz.com.

said on 03 Dec 2005 at 21:20

Thanks a bazillion,asno. This gives me a bit more to grip up on. I’ll be playing with it for the next few weeks when I head back to New Orleans.

Comments are closed for this entry.