Created
February 23, 2012 15:43
-
-
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 file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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. | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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. |
This is a great discussion. Thanks!
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
There's another way to do it without the instance_eval (this is my 'default' pattern in this situation):
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..)