Tuesday, September 18, 2007

Meta-Meta-Meta Programming

Now this isn't particularly clever or something you should even really consider doing but i've wasted my time so now you don't have to :). L and I got a lovely new bookshelf a few weeks ago and I noticed my beloved copy of The Pragmatic Programmer eyeing me off in the corner, so I started to thumb through it.

The astute amongst you that have read it would remember their example on meta-programming that uses Perl to take an ordinary text file and generate Pascal and C source code from it. I thought i'd update it a bit and use Ruby to generate Java, and, well Ruby.

I did it in a similar way to the prag guys i.e. the "proper" way but then I got a little bit curious about the notion of source that generates source that generates source. Plus, I hadn't really dug into Ruby meta-programming and this seemed like something harmless to play with.

Seeing we all love Shoes, let's have a look at a file called Shoe.txt that in pretty much plain English, models a shoe.


1 C Shoe
2 M   brand          String        
3 M   colour         String        
4 M   size           int        7    
5 M   isTrendy       boolean    true    
6 M   scent          Scent        
7 E

For clarification, the beginning of each line identifies what it is modelling:

C is the name of the class
M is a member of the class, specifying it's name, type and default value
E signals the end of the class.

Pretty straight forward, eh.


So how do we turn that into:

Shoe.Java


 1 class Shoe {
 2   private String brand = null;
 3   public String getBrand {
 4     return brand;
 5   }
 6   public void setBrand(String brand)
 7     this.brand = brand;
 8   }
 9
10   private String colour = null;
11   public String getColour {
12     return colour;
13   }
14   public void setColour(String colour)
15     this.colour = colour;
16   }
17
18   private int size = 7;
19   public int getSize {
20     return size;
21   }
22   public void setSize(int size)
23     this.size = size;
24   }
25
26   private boolean isTrendy = true;
27   public boolean getIstrendy {
28     return isTrendy;
29   }
30   public void setIstrendy(boolean isTrendy)
31     this.isTrendy = isTrendy;
32   }
33
34   private Scent scent = null;
35   public Scent getScent {
36     return scent;
37   }
38   public void setScent(Scent scent)
39     this.scent = scent;
40   }
41
42
43 }



and Shoe.rb?


1 class Shoe 
2   attr_accessor :brand, :colour, :size, :isTrendy, :scent
3   def initialize 
4     @colour = nil
5     @size = 7
6     @isTrendy = true
7     @scent = nil
8   end
9 end

Well, there is the smart way i.e. the pragmatic way, or there is the meta-meta-meta programming way.

language_generator.rb


 1 langs = %w(ruby java)
 2 class LangGen 
 3 end
 4
 5 langs.each do |lang|
 6   LangGen.class_eval <<-LETS_DANCE
 7     $first = true
 8     $init = ""
 9  
10     def #{lang}_class_start(name)
11       out = "class " + eval(\"name.chomp\") + ' '
12       out << "{" if '#{lang}' == 'java'
13       out << "\n"
14       out  
15     end
16   
17     def #{lang}_class_end
18       out = ""
19       out << "\n}" if '#{lang}' == 'java'
20       if '#{lang}' == 'ruby'
21         out << $init
22         out << "  end"
23         out << "\nend"
24       end
25       out
26     end
27   
28     def #{lang}_members(name, type, value)
29       out = ""
30       if '#{lang}' == 'java'
31         value ||= 'null'
32         out << "  private " + eval(\"type\")+' '+eval(\"name\")+' = '+
33                   eval(\"value\")+";\n"
34       end
35   
36       out << "" if '#{lang}' == 'ruby'
37       out
38     end
39   
40     def #{lang}_accessors(name, type, value)
41       accessors = ""
42       if '#{lang}' == 'java'
43         accessors << "  public " + eval(\"type\")+
44                         " get"+eval(\"name.capitalize\")+" {\n"
45         accessors << "    return "+eval(\"name\")+";\n"
46         accessors << "  }\n"
47         accessors << "  public void set"+eval(\"name.capitalize\")+ '('+
48                         eval(\"type\")+ ' '+ eval(\"name\")+")\n"
49         accessors << "    this."+eval(\"name\")+" = "+eval(\"name\")+";\n"
50         accessors << "  }\n\n"  
51       end
52       if '#{lang}' == 'ruby'
53         value ||= 'nil'
54         if $first
55           $init << "\n  def initialize \n"
56           accessors << "  attr_accessor :" + eval(\"name\")  
57         end
58         accessors << ", :" + eval(\"name\") unless $first
59         $init << '    @'+ eval(\"name\")+' = '+eval(\"value\")+"\n" unless $first
60         $first = false
61       end
62       accessors
63     end    
64  
65   LETS_DANCE
66 end
67
68 gen = LangGen.new
69 langs.each do |lang|
70   File.open('Shoe.txt').each do |line|
71     if line =~ /^C/
72       print gen.send("#{lang}_class_start".to_sym, line.gsub(/^C\s+/,''))  
73     end
74     print gen.send("#{lang}_class_end".to_sym) if line =~ /^E/ 
75     if line =~ /^M\s+(\w+)\s+(\w+)\s+(\w+)?/
76       name, type, value = $1, $2, $3
77       print gen.send("#{lang}_members".to_sym, name, type, value)
78       print gen.send("#{lang}_accessors".to_sym, name, type, value)
79     end
80   end
81   printf("\n"+'*'*50 + "\n")
82 end

6 comments:

Anonymous said...

I'm disappointed. I was expecting cool meta-programming stuff in the style of Lisp not Perl and Regular-Expressions stuff.

Ah well, this gives me a good topic for a blog post. I'll show you how it's done Lisp-style :P

dan said...

Come back and tell me when you're done, i'd love to see it. Ruby-style Lisp or Lisp-style Lisp? Because if you're going to do it in Lisp then that kind of defeats the purpose of me playing with ruby meta-programming. Still, i'd like to see it though.

Anonymous said...

Posted my version here: http://neverfriday.com/blog/?p=15

It is the Scheme dialect of Lisp and it generates the code using macros. It also processes lists and while I didn't add the file-parsing function, it is trivial since it uses the read macro that Scheme defines.

Stephane Wirtel said...

Why don't you use a template file with ERB ?

dan said...

Looks good Rudolf, i'd still like to see you get the attr_accessors in there :)

dan said...

Stephane, good point. I always thought ERB was more tailored to HTML but it would do a good job with a problem like this and be degrees more readable. I was just generally mucking around. I didn't mean it to be taken as a "proper" or "best" solution to the problem outlined.