stevengharms.com

Sententiae viri ex temporibus duobus

Ottawa Metaprogramming Presentation

Last Tuesday I had the privilege of speaking to the Ottawa Ruby user’s group via remote teleconference. While I would have preferred to be there in person so that I could interact with the attendees (despite the -23C temperature!), I really enjoyed having the chance to speak to these fine folks.

Presentation to Ottawa RUG:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Presentation to Ottawa RUG:

1.  Introduction
  1.  Steven Harms, San Francisco, CA, US
  1.  @sgharms
  1.  http://stevengharms.com
  1.  gh: sgharms
  1.  Employee: shopittome.com
1.  Presentation
  1.  Intro::"What Do We Mean by Metaprogramming" or, Astounding Beginners
    1.  `method_missing`
    1.  Instance methods
    1.  Alternate definition of class methods, methods
  1.  Programming is the art of passing messages (10 mins)
    1. https://speakerdeck.com/sgharms/practical-metaprogramming-ruby
    1.  Standard model
    1.  Metaprogramming
  1.  Standard tools ( 10 mins) and progressing up the ladder
  1.  Advanced usage case study:  a DSL (10 mins)
    1.  https://github.com/sgharms/VerbvectorGenerator/blob/master/lib/verbvector.rb
    1.  https://github.com/sgharms/LatinVerb/blob/master/lib/linguistics/latin/verb/latinverb/verbvector_description.rb
  1.  From the trenches (10 mins):
    1.  Metaprogramming in Rails
    1.  Do's and Don'ts:  Thinking about metaprogramming

What Do we Mean by Metaprogramming Code Samples

Metaprogramming is definitely one of the coolest features of Ruby. I’m a pretty “meta-” thinker, so when I saw Ruby’s capabilities around method_missing and instance variable addition, I was completely hooked. I hoped, by this section to expose the total “nubies” to some of that same magic.

Method Missing Example

One of the fascinating features of Ruby is the ability to not define some method when writing out the class, but rather let the “I don’t know what method that is” error get trapped by method_missing and then, based on some logic, do something other than throw the error. Here we don’t define the methods hiss or slither or strike. Rather we intercept the errors and then creatively handle them. I have no idea why I got on the topic of snakes when writing these examples.

Bonus: Show off that Ruby will let you drop parentheses. Protip: If you care about what comes back (like calling a function) use parents (as I learned from Avdi Grimm); else, feel free to leave them off.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Rattlesnake
  def initialize(species)
    @species = species
  end

  def method_missing(sym, *args)
    puts "dis #{args.empty? ? '' : args.join(', ')+' ' }#{@species} snake will #{sym.to_s}."

    strike_method = [@species, 'strike'].join('_').to_sym
    send(strike_method) if self.respond_to? strike_method and sym.equal?(:strike)
  end

  def cottonmouth_strike
    puts 'a cottonmouth strike is deadly'
  end
end

thing_i_saw_while_camping = Rattlesnake.new 'cottonmouth'
thing_i_saw_while_camping.hiss
thing_i_saw_while_camping.slither
thing_i_saw_while_camping.strike 'dangerous', 'vituperative'

Output:

1
2
3
4
dis cottonmouth snake will hiss.
dis cottonmouth snake will slither.
dis dangerous, vituperative cottonmouth snake will strike.
a cottonmouth strike is deadly

Instance methods

Most programmers familiar with OO will recognize the general pattern that you make a cookie-cutter (a class) and then use it to stamp out instances of that cookies (instances). Methods are defined on the class and all the instances have the ability to call those methods.

The amazing thing about Ruby is that it will let you put methods on an instance. It stuffs those methods in the singleton class (or eigenclass or metaclass). This is where classes stuff their class methods too. When you understand how this works the whole system seems beautiful and elegant and entirely obvious. When you’re new, it seems like magic. Here’s a demonstration of the magic.

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
class Rattlesnake
  def initialize(species, color='unknown')
    @species = species
    @color = color
  end

  def shed
    puts "in a mystical transformation, the #{@color} #{@species} snake sheds"
  end
end

texan_snake = Rattlesnake.new("cottonmouth", 'brown')
texan_snake.shed

coily = Rattlesnake.new("video-game", 'purple')
coily.shed

coily.instance_eval do
  def curse
    '@#$!!?!'
  end
end

puts "Coily can curse #{coily.curse}"
puts "A regular old snake #{texan_snake.respond_to?( :curse ) ? 'does ' : 'does not '} know how to curse."

Rattlesnake.class_eval do
  def curse
    'avadra kedavera!'
  end
end

puts texan_snake.curse
puts coily.curse

Output:

1
2
3
4
5
6
in a mystical transformation, the brown cottonmouth snake sheds
in a mystical transformation, the purple video-game snake sheds
Coily can curse @#$!!?!
A regular old snake does not  know how to curse.
avadra kedavera!
@#$!!?!

Notes

Here all Rattlesnakes know how to shed. The method is applied on the class, no surprise. We create a specific rattler named coily (after Q-bert). On coily we add curse. Then we demonstrate that coily can curse but texan_snake cannot. Then we put a method on the class so that all snakes know how to curse. Now texan_snake can curse and coily, who could already curse, still has his unique curse method because its definition (on the instance) takes precedence over the definition on the class. This is pretty amazing stuff.

Alternate class method definitions; the mystical eigenclass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Ursine
  class << self
    def bare
      puts 'no one likes a bare bear'
    end
  end

  def self.constellation
    puts 'ursa major'
  end

  define_method :raid_bear_box do
    puts 'wants a pick-a-nick basket'
  end
end

Ursine.constellation
Ursine.bare
yogi = Ursine.new
yogi.raid_bear_box

Output:

1
2
3
ursa major
no one likes a bare bear
wants a pick-a-nick basket

Notes:

Standard Ruby pedagogy will teach you about classes which can support methods that the instances will inherit. Also you will be instructed that sometimes you want to put a method on the class itself not on instances. A classic example is something like Math.sin. The sine function will never be instantiated, but it makes sense that its terminology live within a namespace (like Math). The classic way of writing this method would be def Math.sin or, while inside the Math class definition self.sin (which is clearly the same thing). The invocation that got my curiosity up was the class << self block: what’s going on there? It turns out that you’re opening up the singleton_class, the eigenclass.

These were the preliminary exposures to metaprogramming that lead me to read Paolo Perrotta’s book on the topic and to undertake my Rubyconf 2011 presentation.

http://speakerdeck.com/sgharms/practical-metaprogramming-ruby

In that presentation I lead people up a series of steps that should help them become comfortable with rolling metaprogramming into their codebase.

DSLs

The group asked about DSL construction in Ruby. I will provide a bit of context here as to why I did this. I received some feedback via Twitter on this topic and I would like to give a bit more context.

It started simply, I wanted to be able to say in an IRB like shell

@aVerb.active(TAB)indic(TAB)pres(TAB)fir(TAB)sing(TAB) and get

and get

@aVerb.active_voice_indicative_mood_present_tense_first_person_singular_number

So, for love of TAB completion, I wrote a DSL.

As it turns out, I needed to generate something like 160 unique “vectors” that every class needed to know about so that I could TAB-complete. And what if I wanted to have the order be flexible:

@aVerb.active_voice_indicative_mood_present_tense_singular_number_first_person

160 + a lot more methods to describe

And what if wanted to leave off some specifier and get a more general subset?

@aVerb.active_voice_indicative_mood_present_tense_singular_number

or

@aVerb.active_voice_indicative_mood_present_tense

160 + tons of methods to describe.

So I needed something to perform the combinatorics to generate all those vectors and to apply them onto the instance so that I could TAB-complete. So I decided to write a verb-describing DSL. The following two links show my DSL and how it is implemented. In the presentation I suggest that DSLs are best for narrow-use cases. The most easily understood DSL is Ruby programmer-to-programmer, but sometimes it is very helpful to be able to write a DSL so that business-function specific clients can influence the code without knowing Ruby. I think Rails, Cucumber and RSpec have done a pretty good job at demonstrating how well DSLs can work.

  1. verbvector.rb: The DSL implementation
  2. description.rb: The data sent to the interpreter

Conclusion

Thereafter I gave a few general comments about responsible metaprogramming:

  1. Always strive for clarity for other programmers to read
  2. Metaprogramming is not a substitute for good OO

Comments