Advanced Rails Studio: Meta Programming

Chad is giving a very nice presentation walking us through meta programming step by step. You can see the code examples we are creating during his talk, but just looking at the code will note give the whole picture.

# ruby it self uses meta programming
class Person
   attr_accessor :name
end

chad = Person.new
chad.name = 'chad'

# classes are open

# create new class
class Blah
  def greeting
    puts "hello"
  end
end

# reopen class and return id
class Blah
  def do_something!
    greeting
  end
end

b = Blah.new
b.greeting
b.do_something!

# reopen existing class
class String
  def encrypt
    tr "a-z", "b-za"
  end
end

puts "cat".encrypt

# Conceptually ruby (the virtual machine) creates a structure to represent the class
# And this structure can dynamically be defined and changed at runtime.

{
  :String => {:name => "String",
              :methods => {
                :ecryypt => '<method body>',
                :tr  => '<method body>',
                :update => '<method body>',
              },
              :instance_variables => {
                "@name" => "Chad"
              }
  }
}

# replace existing method
class String
  def encrypt
    upcase.reverse
  end
end

puts "cat".encrypt

# Rails extends base classes in activesupport. I.e Fixnum 20.minutes.ago
class Fixnum
  def minutes
    self*60
  end
end

puts 20.minutes

class Fixnum
  def from_now
    Time.now + self
  end
  def ago
    Time.now - self
  end
end

puts 20.minutes.from_now
puts 20.minutes.ago


# Class definition are executed line by line
class Chad
  #exit  #uncomment this and the program will halt here!
  puts "Hello, defining #{self}"
end
puts Chad.new.inspect

# Can conditionaly create class
class Chad
  #exit  #uncomment this and the program will halt here!
  puts "Hello, defining #{self}"
  puts "Type OK when prompted"
  response = gets.chomp
  if response == "OK"
    def greeting    
      puts "OK"
    end
  else 
    def greeting    
      puts "O NO!!!"
    end
  end  
end

puts Chad.new.greeting
# Could use this to have different code for RAILS_ENV is "Prodution" or "Development"
# Sending messages to Object. Object receive message, all method calls have received

"Chad".upcase         #,I.e. String "Chad" gets message upcase
puts "Hello"          # event 'puts' is a message
puts self.class.name  # Even when running script, running in context of an Object

class Person
  def initialize(name)
    @name = name
    greeting
    puts "self.inspect: #{self.inspect}"
  end
  
  def greeting
    puts "0, hello #{@name}."
  end
  
end
puts Person.new('daniel').greeting

# Calling class methods
class Person
  puts "Puts is send to self. Self is person:#{self} when defining class."
end

# Can point to a class
person_class = Person
puts person_class.class

class Man
end

class AstroMan
end

# Factory method, classes are just object that can be passed around
def man_or_astroman
  klass = (rand(2) > 0 ? Man : AstroMan)
  klass.new
end

puts man_or_astroman
puts man_or_astroman
puts man_or_astroman
puts man_or_astroman

# Constance in Ruby are not Constants
if false  #Don't run this
  String = "HAHAHA"   #You can even change the class constants implementation
  # You'll get a warning (Warning: already initialized constant String), but you can change it
  Integer = "bla"
  Array = 123
end

# Methods can be defined on Objects and not just Classes
animal = "Cat"
def animal.speak
  "woof"
end 
puts "animal.speak: #{animal.speak}"
# "dog".speak doesn't exists, only the specific animal instance has speak
# It's not often done in Ruby, but in another context you'll do it all the time...

# .. Adding class methods (singleton methods (not related to pattern)).
class Human
    def self.announce_self
        puts "I AM #{self}, and I AM BEING DEFINED"
    end
    announce_self # Can invoked defined class method while defining class
end

# Same as doing def Human.annouce_sef end
Human.announce_self

# We are getting closer to has_many
class Superman < Human
  announce_self
end

# Let's try doing something similar to ActiveRecord
module ActiveRecord
  class Base
    def self.has_many(*things)
      puts "#{self} has_many #{things}"
    end
  end
end

class BuzzLightYear < ActiveRecord::Base
  has_many :space_ships # does nothing for now but it's valid syntax
end
# This is more the way Rails works
# Can do included hook and extend
module ActiveRecord
    module Associations
        module HashManyAssocation
          def self.included(klass)
            klass.extend(ClassMethods)
          end
          module ClassMethods
            def has_many(things, options = {})
              # TODO: define methods
              puts "#{self} has many #{things}"
            end
          end
        end
    end
end

module ActiveRecord
  class Base
    # INCLUDE
    include ActiveRecord::Associations::HashManyAssocation  
  end
end

class AstorMan < ActiveRecord::Base
  has_many :space_ships
end
# But could simply extend
module ActiveRecord
    module Associations
        module HashManyAssocation
            def has_many(things, options = {})
              # TODO: define methods
              puts "#{self} has many #{things}"
            end
        end
    end
end

module ActiveRecord
  class Base
    # EXTENDS
    extend ActiveRecord::Associations::HashManyAssocation
  end
end

class AstroMan < ActiveRecord::Base
  has_many :space_ships
end

# acts_as_ ... to add functionality without extend ActiveRecord::Base
# We could use include with the included hook

module SuperHero
  def self.included(klass)
    klass.extend(ClassMethods)
  end
  module ClassMethods
    def acts_as_superhero
      puts "I'm a bird, I'm a plane, no I'm #{self}"
    end
  end
  def fight_crime
    puts "OK, fighting crime"
  end
end

# use include
class AstroMan < ActiveRecord::Base
  include SuperHero
end
AstroMan.new.fight_crime

# And include in your base class
class ActiveRecord::Base
  include SuperHero
end

# then acts_as is available
class SuperMan < ActiveRecord::Base
  acts_as_superhero
end
SuperMan.new.fight_crime
# Calling non-existant methods
class Chad  
  def method_missing(method_name, *args)
    puts "You called #{method_name} with #{args.inspect}"
  end
end
Chad.new.just_do_it('again', 'and again')

# Calling non-existant classes
def Object.const_missing(name)
  puts "Trying to get to non existing clas #{name}"
  # trick: could require the file if class is missing
end
AnythingClass
# Now that we went through the concepts let's do some meta programming

# eval
def evaluator(str, a_binding)
  a_value = 123
  eval(str, a_binding)
end

str = "puts a_value"
a_value = 321
evaluator(str, binding)  # -> 321. binding is the current scope of the program
evaluator(str, nil)      # -> 123. don't pass binding

# instance_eval
class Thing
    def a_value
        123
    end
end

Thing.new.instance_eval("puts self.a_value") # -> 123. run in context of an instance

# Two more 'eval': class_eval and module_val

#class_eval 
class Person
end

Person.class_eval do  # Be in context of class
  p self            # -> Person
  def greeting
    puts "Hello"
  end
end

Person.new.greeting  # defining instance method

def add_greeting_to(klass)
  klass.class_eval do
    def greeting
      puts "Greeting"
    end
  end  
end
add_greeting_to(String)
"asdf".greeting    # -> Greeting

# module_eval is basically same thing as class_eval

# define_method
class Chad
  define_method(:foo) do |arg1|
    puts "hello, #{arg1}"
  end
end

Chad.new.foo(:bar)

Posted by Daniel Wanja Fri, 13 Jun 2008 16:57:00 GMT


Comments

  1. Oscar Del Ben about 4 hours later:

    Great work! Thanks for share

  2. Sebastian about 1 month later:

    Do you use metaprogramming in every day apps or is just a specjal feature?