Skip to content

Instantly share code, notes, and snippets.

@bokmann
Created February 23, 2012 15:43
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save bokmann/1893359 to your computer and use it in GitHub Desktop.
Save bokmann/1893359 to your computer and use it in GitHub Desktop.
The code of the talk from my Feb 22nd Arlington Ruby talk 'There is No Such Thing as Metaprogramming'.
# This is the code from my 'There is No Such Thing as Metaprogramming' talk,
# which premiered at the Arlington, VA Ruby Users Group on Feb 22nd.
# Without the deliver and walk-through to the solution below this example
# will be missing quite an important bit of content (mainly the tracking of
# 'self' while developing the solution, but it still a useful read.
# Here is the Toddler with no metajuju. Note that the developer, as well as
# the code, is completely unuaware of the interpreter. A developer with a
# background in compiled languages would be comfortable looking at this.
class Toddler
def play_dragonvale
puts "Dragonvale is fun!"
end
def watch_octonauts
puts "I'm watchin me some great Octonauts"
end
end
anthony = Toddler.new
anthony.play_dragonvale
anthony.watch_octonauts
#Now lets execute this line:
anthony.public_methods.sort
# is THAT metaprogramming? No, but it is code that is starting to reason
# about its own existence...
# In this example, are we metaprogramming yet? I don't think this is 'code
# that writes code' any more than the above example is, but it is 'code that is
# aware of the interpreter interpreting it'. A compile-driven developer would
# have a hard time reading this, but to the interpreter it is pretty much the
# same thing as above.
Object.const_set(:Toddler, Class.new).class_eval do
define_method("play_dragonvale") do
puts "Dragonvale is fun!"
end
define_method("watch_octonauts") do
puts "I'm watchin me some great Octonauts!"
end
end
anthony = Toddler.new
anthony.play_dragonvale
anthony.watch_octonauts
# and in the ~15 minutes of the talk, we work towards this solution. Notice that
# the end result of the Toddler class is compatible with the example above. This
# example by itself is the end result of the talk, but the important part of my
# talk is the journey getting here. The Nerd, Jock, and Toddler class have been
# written in a style sometimes called 'declarative programming' or an 'internal
# domain specific language'.
module Personality
def self.included(base)
base.instance_eval do
def watches(entertainment_source)
define_method("watch_#{entertainment_source}") do
puts "I'm watchin me some great #{entertainment_source}!"
end
end
def plays(game_type)
define_method("play_#{game_type}") do
puts "#{game_type} is fun!"
end
end
end
end
end
class Nerd
include Personality
watches :star_trek
watches :stargate
plays :minecraft
end
class Jock
include Personality
watches :football
plays :football
plays :beer_pong
end
class Toddler
include Personality
watches :octonauts
plays :dragonvale
end
dave = Nerd.new
dave.watch_star_trek
# I'm watching me some great star_trek!
dave.play_minecraft
# Minecraft is fun!
peter = Jock.new
peter.watch_football
# I'm watchin me some great football!
peter.play_beer_pong
#Beer Pong is fun!
anthony = Toddler.new
anthony.play_dragonvale
anthony.watch_octonauts
# as your own exercise, write a 'tells' method in personality so we can say
# things like:
tells :knock_knock_joke
tells :dirty_joke
tells :blonde_joke
# My major assertion of this talk is that people define Metaprogramming as 'code
# that writes code'. I assert that in Ruby, because we have the interpreter, all
# our code does that. In the first Toddler example, neither the code nor the
# developer is aware of that. In the second toddler example the code is aware of
# it, and in the Personality module we are using that knowledge of
# interpretation to our advantage. In Ruby, its not such much that we can 'write
# code that can write code', but that we can 'write code that reasons about its
# own interpretation'. This isn't metaprogramming, its just 'Ruby programming'.
# The term 'metaprogramming' is the lie that lets people see the truth - but it
# also sets up a barrier that people need to tear down, otherwise they are just
# compiler-driven developers in an interpreted language.
#
# Finally, we draw a lot of observations from the process of writing this
# code... notice that we have taken some complexity that was spread out amongst
# the original Toddler class and concentrated it into a module, leaving the new
# Toddler class very simple and self-descriptive. This technique for bundling up
# reusable code is powerful, and Rails itself is a great example of that. How
# many people, as they were learning Rails, didn't know or care whether
# 'has_many' or 'validates_presence_of' was a language keyword or a method call?
# The tradeoff: we are lowering the bar of our API users and raising the bar of
# our library maintainers.
# I did this talk again tonight at the Philly.rb user group, and who should
# walk in? None other than wycats himself. Talk about pressure.
#
# He brought up a really good point that I'd like to bring up here, but then
# discuss why I chose to write this example as I did. His point boiled down
#to extend vs. include, and changes the code example above to this:
module Personality
def watches(entertainment_source)
define_method("watch_#{entertainment_source}") do
puts "I'm watchin me some great #{entertainment_source}!"
end
end
def plays(game_type)
define_method("play_#{game_type}") do
puts "#{game_type} is fun!"
end
end
end
class Toddler
extend Personality
watches :octonauts
plays :dragonvale
end
anthony = Toddler.new
anthony.play_dragonvale
anthony.watch_octonauts
# the major changes being the removal of two lines chock full of complexity
# from the module:
# def self.included(base)
# base.instance_eval do
#
# and the change of the class definition from include to extend. Definitely
# cleaner, definitely less complex, and he added the statement, "by using
# extend, its clear to users you are adding methods to the class."
#
# Yehuda is right on all points. I want to make a couple of counterpoints.
#
# First, I want you to think back to when you first started using Rails. You
# saw magic things like 'validates_presence_of' and 'belongs_to', and it
# didn't matter to you that those were actually class methods - they looked
# like keywords in the language. Yes, it was magical, but it didn't matter to
# you, it was just 'easy to learn and use'. Signaling to me with 'extend'
# something about the implementation of the module isn't compelling enough on
# its own, considering the point of this exercise is hiding the
# implementation.
#
# Second, the point of the module isn't to add behavior to the class - that is
# a consequence of implementing something as a domain specific language. The
# point of this exercise is to add behavior to the instance... the
# play_dragonvale method... so that seems to me a vote for include - to signal
# I'm adding behavior to the instance (ignoring that I have effectively 'added
# new keywords' to help the DSL user do that).
#
# Third, removing those two lines certainly simplifies this example. I take
# that very seriously, as that kind of 'muddy manipulation' of 'self' thats
# going on there is what makes this stuff so damn hard. removing it is clearly
# a win for maintainability. If I'm going to ignore that, there has to be a
# very compelling reason.
#
# Finally, I think I have one... the actual keyword 'include' vs. 'extend'.
# Reading this as a pure domain specific language, forgetting the fact that
# its Ruby, would you want to say that a Toddler 'includes' a personality, as
# if you are building it up via composition, or that a Toddler 'extends' a
# personality? Lets talk about another example to get some perspective, the
# classic OO strawman of Automobile.
#
# If you were defining an Automobile and wanted to have modules that added a
# DSL-like syntax for adding a navigation system, a luxury option like a kid's
# DVD player, etc would you want that DSL to read
class Automobile
extend :nav_system
extend :dvd_player
end
# or
class Automobile
include :nav_system
include :dvd_player
end
# to me, the answer is pretty clear - my car isn't a dvd_player, but it
# includes one. A couple of lines of complexity to be maintained by the
# person who took on the responsibility in the first place, in order to make
# the DSL more expressive, is a design choice I'm willing to make in some
# circumstances.
#
# And it seems I'm not the only one who thinks this way, as you can find
# examples 'in the wild' of this technique in use. The first one I can
# find without looking very far is the acts_as_state_machine gem:
#
# https://github.com/rubyist/aasm
#
# the curator gem does it as well:
#
# https://github.com/braintree/curator
#
# Finally, notice that on Yehuda's own blog entry about using
# ActiveModel::Validations, exactly this technique of adding class-level
# 'keyword methods' using include is used to add 'validates_presence_of' to
# his 'plain old ruby object' Person (the second person example):
#
# http://yehudakatz.com/2010/01/10/activemodel-make-any-ruby-object-feel-like-activerecord/
#
# Validations is actually using ActiveSupport::Concern to do this, as
# opposed to the included hook and an instance_eval, but check out the code:
#
# https://github.com/rails/rails/blob/master/activesupport/lib/active_support/concern.rb
#
# notice line 116, the use of the included hook? notice line 111 and 112,
# the extend and the class eval? Finally, as an exercise left to the reader,
# where is that append_features method defined on line 103 actually called?
# Isn't this doing essentially the same thing I'm doing, adding class-level
# methods through an include?
#
# What happens if I don't want to bring along all of ActiveSupport for this
# one thing, as I might not want to in a simple gist geared towards teaching
# metaprogramming?
#
# To be clear, I'm not picking on Yehuda - his words inspired me to dig even
# deeper into the subject and for that I thank him. I think that the
# technique I'm teaching here is in use in the wild, and understanding it will
# remove the mystery in many pieces of code you might see, including Yehuda's.
@peterc
Copy link

peterc commented Apr 12, 2012

There's another way to do it without the instance_eval (this is my 'default' pattern in this situation):

module Personality
  module ClassMethods
    def watches(foo); puts foo; end
    def plays(foo); puts foo; end
  end

  def self.included(base)
    base.extend ClassMethods
  end
end

class Nerd
  include Personality

  watches :foo
  plays :bar
end

I have similar cosmetic concerns about 'include' vs 'extend' (although I don't think they're very rational, but still!) but I also like to follow a generic pattern that gives me an option to do both instance and singleton methods, which this does. Indeed, I think I learned this pattern from seeing it a lot in code several years ago.. no idea if it's the most popular route now or not though.

(Thanks to Yehuda for pointing out my initial mistake in calling the submodule InstanceMethods - oops! The instance methods can just reside in the outer module, of course..)

@brandonhilkert
Copy link

This is a great discussion. Thanks!

@wycats
Copy link

wycats commented Apr 12, 2012

The code version of some of my comments last night: https://gist.github.com/2369717

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment