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.
@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