Abstract
Using string-eval in Ruby for metaprogramming is unnecessarily obscuring. Ruby’s more modern
and specific metaprogramming methods should be used instead whenever possible. This problem
is illustrated on the example of Ruby’s Forwardable
class.
In detail…
Ruby’s Forwardable
class is using metaprogramming to forward calls from a frontend interface to an instance in the back
executing the call.
Metaprogramming is the discipline of making code that creates code. This task allready is rather
abstract and hard to grasp in itself. Having hard to grasp code is a liability. One of the goals
of writing code is allways to keep the code as simple and as well understandable as possible.
Additionaly, metaprogramming code itself is difficult to read and understand: that is because the
metaprogramming code will not necessarily express what the code it is creating is about, but only
how it is creating that code. As such the code it is creating can be invisible to you as a reader
of the source code - the created code will only start to exist at runtime.
One would therefore expect that programmers would try especially hard when they metaprogram to make
that particular kind of code expressive and easy to understand.
Another consequence of the fact that the code produced by metaprogramming is not necessarily visible,
is that debugging becomes more difficult: when analyzing problems you’ll not only be unsure how the
programm works, but in addition, you won’t even be sure how the code that is executed looks like -
since it is only generated at runtime.
This post is focusing on the last problem: debugging of metaprogrammed code.
There are two approaches to metaprogramming. One is to have as far as possible compile-time parseable
code and the other is to let the code only be parsed at runtime.
As of version 1.9.2, Ruby’s Forwardable
class is using the
latter. The metaprogramming code in Ruby 1.8.7 looks like this:
module_eval(<
As said, this has the consequence of the metaprogrammed code being completely invisible to the parser and other
tools such as editors and debuggers.
This results in the following:
$ cat queue.rb
require 'rubygems'
require 'forwardable'
require 'ruby-debug'
class Queue
extend Forwardable
def initialize
@q = [ ] # prepare delegate object
end
# setup preferred interface, enq() and deq()...
def_delegator :@q, :push, :enq
def_delegator :@q, :shift, :deq
# support some general Array methods that fit Queues well
def_delegators :@q, :clear, :first, :push, :shift, :size
end
q = Queue.new
debugger # ------ DEBUGGING FROM HERE ON -----
q.enq 1, 2, 3, 4, 5
q.push 6
q.shift # => 1
while q.size > 0
puts q.deq
end
q.enq "Ruby", "Perl", "Python"
puts q.first
q.clear
puts q.first
$ ruby queue.rb
queue.rb:24
q.enq 1, 2, 3, 4, 5
(rdb:1) step
(__FORWARDABLE__):2
(rdb:1) list =
*** No sourcefile available for (__FORWARDABLE__)
(rdb:1) step
(__FORWARDABLE__):3
In other words, you are rather lost allready - otherwise you probably wouldn’t be stepping through
your code - and in that situation it happens that your debugger gets completely lost as well,
since it does not know any more where in the code it is and what it exactly is executing.
That’s nothing the programmer wishes for. In a situation where you are debugging you want to have
a maximally clear view of all state, including what code you are currently executing.
Chaning that situation requires making as much of the metaprogrammed code visible to the parser,
which is the second approach to metaprogramming mentioned previously:
$ cat forwardable2.rb
...
self.send(:define_method, ali) do |*args,█|
begin
instance_variable_get(accessor).__send__(method, *args,█)
rescue Exception
[email protected]_if{|s| /^\\(__FORWARDABLE__\\):/ =~ s} unless Forwardable2::debug
Kernel::raise
end
end
Note that it’s the same code as before, except that we do not do eval("string")
any more,
but instead are using specific, more modern metaprogramming tools provided by standard Ruby.
The result is the following:
$ ruby queue.rb
queue.rb:22
q.enq 1, 2, 3, 4, 5
(rdb:1) step
/usr/lib/ruby/1.8/forwardable2.rb:149
begin
(rdb:1) list =
[144, 153] in /usr/lib/ruby/1.8/forwardable2.rb
144 accessor = accessor.id2name if accessor.kind_of?(Integer)
145 method = method.id2name if method.kind_of?(Integer)
146 ali = ali.id2name if ali.kind_of?(Integer)
147
148 self.send(:define_method, ali) do |*args,█|
=> 149 begin
150 instance_variable_get(accessor).__send__(method, *args,█)
151 rescue Exception
152 [email protected]_if{|s| /^\\(__FORWARDABLE__\\):/ =~ s} unless Forwardable2::debug
153 Kernel::raise
(rdb:1) step
/usr/lib/ruby/1.8/forwardable2.rb:150
instance_variable_get(accessor).__send__(method, *args,█)
Allready much, much better.
Of course, with the string-eval approach to metaprogramming Ruby itself could do better by saving the string that is being
evaled to be able to refer to it later at step-through time. However currently we don’t have this option.
Tomáš Pospíšek