Wakatta!

Like Eureka!, only cooler

Seven Languages in Seven Weeks Ruby Day 3

Third and final day on Ruby. This time, metaprogramming techniques are covered.

Metaprogramming allows a program to write programs, or more interestingly, to modify itself. The structure of the running program is made available to introspection API, and can be updated or extended.

Ruby as a really powerful set of tools for metaprogramming, but a good understanding of Ruby’s metamodel and some of its darker corners is required to fully benefit from them.

Exercises

But first let’s finish the homework (day 3 has only a short one).

Improved Acts as CSV module

Acts as CSV module (acts_as_csv_module.rb) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
module ActsAsCsv
  def self.included(base)
    base.extend ClassMethod
  end

  module ClassMethod
    def acts_as_csv
      include InstanceMethods
    end
  end

  module InstanceMethods
    def read
      @csv_contents = []
      filename = self.class.to_s.downcase + '.txt'
      file = File.new(filename)
      @headers = file.gets.chomp.split(', ')

      file.each do |row|
        @csv_contents << row.chomp.split(', ')
      end
    end

    attr_accessor :headers, :csv_contents

    def each(&block)
      @csv_contents.each {|r| block.call(CsvRow.new(@headers, r)) }
    end

    def initialize
      read
    end
  end
end

class CsvRow
  def initialize(h, r)
    @headers = h
    @row = r
  end

  def method_missing name, *args
    h = name.to_s
    @row[@headers.index(h)]
  end
end

class RubyCsv
  include ActsAsCsv
  acts_as_csv
end

The code is fairly straightforward. The new class CsvRow does most of the job. It uses method_missing to access the relevant column. There is no error checking, so please don’t make mistakes…

The codes behaves as intended:

1
2
csv = RubyCsv.new
csv.each {|row| puts row.one}

does print

1
lions

As an alternative, the code below creates the methods during the initialization of the instance. There could (should?) be an easier way, but I could not find one. The new methods are added to the singleton class, so each instance has its own set.

Using define_method rather than method_missing
1
2
3
4
5
6
7
8
9
10
class CsvRow
  def initialize(h, r)
    @headers = h
    @row = r
    singleton = class << self; self; end
    h.each do |field|
      singleton.send(:define_method, field.to_sym) { r[h.index(field)] }
    end
  end
end

The code uses the send method because define_method must be used from within the class (it is a private method), but when I open the class I change the scope and loose access to the original parameters h and r.

With such modification, the codes still executes as required.

Wrapping up day 3

This chapter was short, certainly, but it gives a tantalizing overview of metaprogramming.

However, these techniques bring to light the fact that Ruby does not have definitions, only code that defines things, and that the evaluation order of this code matters. This becomes clearer when trying to modifies classes as they are being define.

Consider the following fragment. The Path class does nothing really important, but it could for instance wrap methods with a proxy. For this it needs to know the methods that are defined on the target class.

Target1 and Target2 both define the same methods (through the use of attr_accessor), but Target1 includes Patch first, then define the attribute, while Target2 includes Patch last.

Evaluation order matters (meta.rb) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module Patch
  def self.included(klass)
    puts klass.instance_methods.member?(:my_attribute)
  end
end


class Target1
  print "In Target1: "
  include Patch
  attr_accessor :my_attribute
end

class Target2
  print "In Target2: "
  attr_accessor :my_attribute
  include Patch
end

When executed, the code produces

1
2
In Target1: false
In Target2: true

So in Ruby, it is fair to say that there are no declarations, only instructions, all of them executed in order (some of these instructions create functions, classes, or blocks to be executed later).

Indeed, the following program fails to execute:

Evaluation order really matters… (eval.rb) download
1
2
3
4
5
hello()

def hello
  puts "Hello, world"
end

while the equivalent Perl one succeeds:

… or not depending on the language (eval.pl) download
1
2
3
4
5
hello();

sub hello {
  print "Hello, world\n";
}

This is because Perl processes the definitions first, then executes the instructions in order.

Ruby’s execution mode is similar to Common Lisp’s. Actually, Common Lisp makes is even more complex by virtue of being a compiled language with various phases (eval, compile and load), allowing (and sometimes requiring) selective evaluation of various parts of the code. Hopefully Ruby metaprogramming will not be that complex.

Still, despite the potential for obfuscation, metaprogramming (combined with Ruby’s low ceremony syntax) supports the creation of elegant DSL and simplifies program architectures. It is a way to centralizes complexity, and drain it from the rest of the code.

About Ruby

I really like Ruby. Even as the bastard child of Perl and Smalltalk that it is, it has a level of consistency and cohesion that well thought. Each of its shortcoming (Ruby can be rather slow, and as noted above metaprogramming can become very complex) is a reasonable trade off, and it can be argued that the advantages these trades off bought more than compensate for the shortcomings.

More importantly, the Ruby ecosystem is bristling with interesting tools and ideas, and it really is fascinating to explore.

Despite its different origin, Ruby is a Lisp for the 21st century.

Comments