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.
But first let’s finish the homework (day 3 has only a short one).
Improved Acts as CSV module
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
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:
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.
1 2 3 4 5 6 7 8 9 10
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
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.
Target2 both define the same methods (through the use of
Patch first, then define the attribute, while
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
When executed, the code produces
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:
1 2 3 4 5
while the equivalent Perl one succeeds:
1 2 3 4 5
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.
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.