Ruby Metaprogramming Tutorial Part 1 - Introduction and Background
Introduction
Ruby provides a convenient syntax for metaprogramming but it is often unclear when other libraries are using metaprogramming
Ruby is reflective, you can take a look at the properties of all objects.
Ruby’s facilities for DSL authoring don’t change the nature of the language. Ruby’s metaprogramming facilities are inherently tied to Ruby syntax and semantics, and whatever you write has to be shunted into Ruby’s object model.
The often repeated Ruby mantra is “Don’t Repeat Yourself” (DRY). In Ruby this is mainly achieved through the use of Metaprogramming. Metaprogramming is a technique that allows us to write code that returns code that we can use. We will start with a simple example.
class Simple
def hello
puts "Hello"
end
def method_missing(name, *args, &block)
puts "tried to handle unknown method %s" % name
unless args.empty?
puts "it had arguments: %p" % [args]
end
end
endWe can try loading that class in irb and running a few
methods on it.
irb(main):013:0> Simple.new.hello
Hello
=> nil
irb(main):014:0> Simple.new.goodbye
tried to handle unknown method goodbye
=> nil
irb(main):015:0> Simple.new.goodmorning("Julio")
tried to handle unknown method goodmorning
it had arguments: ["Julio"]
=> nilThe method_missing method catches any
method that we attempt to call in an instance of the
Simple that is not previously declared. If the method does
not exist in Simple then method_missing will
be called with all of the details of the non-existent method and print
its values to the stdout.
This provides us a way to handle methods we have not defined within in the class. We can use particular details about the method name, arguments or code block to decide what to do next.
Background
Before exploring Metaprogramming further, we need to understand the
Ruby object system and introspection. In Ruby, we do everything in the
context of an object. Even when we run irb and run code at
the top level, we are actually working in main of class
Object. At anytime you add methods to a ruby class or
override existing methods.
class and instance variables
Class variables are available directly from a class to a class or an
instance. Instance variables require making an instance with new and
then the instance have access to the variables. Users only have direct
access to exposed class and instance variables. However, you can still
access private variables through Object methods. Class
variables only have one value per class and subclass hierarchy.
class Animal
@@legs = 2
def self.legs
@@legs # class variable getter
end
end
puts Animal.legs # => 4Even when you inherit a class with class variables and change the value in the subclass, the value update will be reflected in the parent class as well. The class variable is shared between the parent and the subclasses.
class Dog << Animal
@@legs = 4
end
puts Animal.legs # => 4
puts Dog.legs # => 4We will remake the dog class with its own class and instance
variables. We expose the @name instance variable via
attr_accessor.
class Dog
@@legs = 4
@name
attr_accessor :name # create getter and setter for Dog
end
d = Dog.new
d.name = "Fido"
puts d.instance_variables # => [:@name]class and instance methods
Class methods are available without making an instance and require
the self prefix. They cannot access instance variables.
Instance variables are available after creating an instance with
new. They can access class and instance variables.
class Foo
def self.bar
puts 'class method'
end
def baz
puts 'instance method'
end
endinclude
The include statement mixes in a module into a class. It
is a simple form of multiple inheritance and forms a “is-a”
relationship. It makes its methods available to an instance of the class
that includes it.
module Foo
def foo
puts "Hello from Foo"
end
end
class Bar
include Foo
end
Bar.new.foo
Hello from Fooextend
The extend statement makes a module’s methods to class.
We can call the methods directly from the class without making a new
instance.
class Baz
extend Foo
end
Baz.foo
Hello from Fooself
self is a contextual pointer. It points to the current
object the program is located in. When we are in the top level of the
file, we are located in the main object.
> puts self
main
> puts self.class
ObjectWhen we declare a method at the top level, it becomes part of the
main object. We can see that :m is a method of
main.
def m
end
puts self.method(:m)
=> #<Method: Object#m>
self.methods
=> [:m ...]In an object or module self is the current object.
class S
def put_self
puts self
end
end
> S.new.m
#<S:0x007fad47294db0>
module Library
puts self
end
LibraryBasicObject
BasicObject1 is the parent object of
all objects in Ruby. It can be used to create alternative object
hierarchies in Ruby. The main hierarchy in Ruby is Object
that includes Kernel. It provides a small number of
methods.
There are four callback methods that are useful for metaprogramming.
method_missingsingleton_method_addedsingleton_method_removedsingleton_method_undefined
Kernel
Kernel2 provides a number of useful methods
such as Array, Hash, Integer,
String, open, puts, etc.
Kernel is mixed into Object and
main is of class type Object so all of these
methods act as global methods in Ruby.
These methods are useful for metaprogramming.
evalexeclambdalocal_variables
Object
Object3 is the main object in the Ruby
object hierarchy. It includes Kernel and is a
child of BasicObject. Object has a large
number of useful methods for metaprogramming.
define_singleton_method extend instance_of?
instance_variable_defined? instance_variable_get instance_variable_set
instance_variables is_a? kind_of?
method methods private_methods
protected_methods public_method public_methods
public_send remove_instance_variable respond_to?
respond_to_missing? send