Metaprogramming Ruby
I Love the book “Metaprogramming Ruby” by Paolo Perrotta and found it very informative. The idioms defined in the book are so helpful. Here I have created a reference based on them for my own use. Hopefully it will help others too.
1) Dynamic Dispatch
Ruby allows us to dynamically call unknown methods(even private methods) on objects.
# object.send(message, *arguments) 2.send(:+, 3) # => 5
2) Pattern Dispatch
Similar to Dynamic Dispatch, but uses a convention or pattern to identify which methods to call.
class User attr_accessor :first_name attr_accessor :last_name def full_name "#{first_name} #{last_name}" end end user = User.new user.first_name = "Ranjithkumar" user.last_name = "Ravi" # use pattern dispatch to invoke all 'name' methods user.public_methods.each do |method_name| puts "#{method_name} = #{user.send(method_name)}" if method_name =~ /_name$/ end # -- output -- # first_name = Ranjithkumar # last_name = Ravi # full_name = Ranjithkumar Ravi
3) Dynamic Method
Ruby allows us to dynamically create methods at runtime
class Bar def self.create_method(method) define_method "my_#{method}" do puts "Dynamic method called 'my_#{method}'" end end # these methods are executed within the definition of the Bar class create_method :foo create_method :bar end # Test out our dynamic methods Bar.new.respond_to? :my_foo # => true Bar.new.my_foo # => "Dynamic method called 'my_foo'" Bar.new.my_bar # => "Dynamic method called 'my_bar'"
4) Ghost Method
When a method is not found, Ruby will send this method as a symbol to method_missing.
class Example def method_missing(method_name, *args) puts "You called: #{method_name}(#{args.join(', ')})" puts "You also passed a block" if block_given? end end Example.new.this_is_cool(1, 2, 3) # => You called: this_is_cool(1, 2, 3) Example.new.this_is_cool(:a, :b, :c) { puts "a block" } # => You called: this_is_cool(a, b, c) # => You also passed a block
5) Dynamic Proxy
Wrapping an object or service and then forwarding method calls to the wrapped item is known as dynamic proxying.
In other word, Catching Ghost Method
and forwarding them onto another method/service.
def method_missing(method_name, *args, &block) return get($1.to_sym, *args, &block) if method_name.to_s =~ /^get_(.*)/ super # if we don't find a match then we'll call the top level `BasicObject#method_missing` end
If we find a match for get_#{name} then we will delegate to another method such as get(:data_type) where :data_type is :name or :age(e.g. get_name, get_age etc) else send to super and raise error.
6) Blank Slate
Ruby allows us to remove functionality from a class. This technique can be useful to ensure that your class doesn’t expose unwanted or unexpected features.
e.g. Prevents issues when using “Dynamic Proxy”. User calls a method that exists higher up the inheritance chain so your method_missing
doesn’t fire because the method does exist. To work around this issue, make sure your class starts with a “Blank Slate”.
To remove method, use Module#undef_method
(removes all the methods), or Module#remove_method
(remove receiver’s method, keep inherited methods). Ghost methods are slower than normal methods. Do not remove methods start with __, method_missing or respond_to?, and leave some other methods.
# create a blank slate class class ImBlank public_instance_methods.each do |method_name| undef_method(method_name) unless method_name =~ /^__|^(public_methods|method_missing|respond_to\?)$/ end end # see what methods are now available ImBlank.new.public_methods # => ["public_methods", "__send__", "respond_to?", "__id__"]
7) Kernel Method
Defining methods in the Kernel module will make those methods available to all objects.
module Kernel def say_hello puts "hello from #{self.class.name}" end end Class.say_hello # => hello from Class Object.say_hello # => hello from Class Object.new.say_hello # => hello from Object 1.say_hello # => hello from Fixnum "".say_hello # => hello from String
8) Scope
Class.new is an alternative to class
Scope Gate:
There are 3 ways to define a new scope in Ruby:
- starting new class definition,
class
- starting new module definition,
module
- start new method,
def
Global variable can access any scope. Be aware that scoping in Ruby is different than some other languages. Ruby does not chain scopes when performing lookups, so don’t expect it to find variables defined in an outer scope.
scope = "main scope" puts(scope) # => main scope class ExampleClass # the main scoped variable isn't defined in the classes' scope defined?(scope) # => nil scope = "class scope" puts(scope) # => class scope end
Flattening the scope:
Where you change the code in such a way that it’s easier for you to pass variables through “Scope Gates”.
my_var = "abc" MyClass = Class.new do puts "#{my_var} in class" define_method :my_method do puts "#{my_var} in method" end end # => abc in class MyClass.new.my_method # => abc in method
9) Eigenclass
A hidden class on the ancestors chain. Eigenclass is a singleton class object. It stores the singleton method of an object.
Class Extension:
class MyClass class << self def my_method; 'hello'; end end end MyClass.my_method # => "hello"
Object Extension:
module MyModule def my_method; 'hello'; end end obj = Object.new class << obj # extends obj include MyModule end obj.my_method # => "hello" obj.singleton_methods # => [:my_method] # Another way to extend object obj = Object.new obj.extend MyModule obj.my_method # => "hello"
10) Context Probe
Execute a code block in the context of another object using instance_eval
class Foo def initialize @z = 1 end end foo = Foo.new foo.instance_eval do @z = 2 puts @z # => 2 end # There is also `instance_exec` which works the same way but allows passing arguments to the block Foo.new.instance_exec(3) { |arg| @z * arg } # => 3
11) Clean Room
Clean rooms are used to change the current context to something expected or clean (does not affect to current environment).
def do_stuff @scope end @scope = "outer scope" puts do_stuff # => outer scope Object.new.instance_eval do @scope = "clean room scope" puts do_stuff # => clean room scope end
12) class_eval
Evaluate a block in the context of a class. Similar to re-opening a class but more flexible in that it works on any variable that references a class, where as re-opening a class requires defining a constant.
def add_method_to(a_class) a_class.class_eval do def m; 'Hello!'; end end end add_method_to String 'abc'.m # => "Hello!"
13) Class Macro
Class Macros are just regular class methods that are only used in a class definition.
e.g. attr_accessor
, attr_reader
. These are class macros.
Write your own class macro. Here is an example of deprecate old methods, print warning message when being called.
class Book def self.title; puts "I'm an A" end def self.deprecate(old_method, new_method) warn "Warning: #{old_method}() is deprecated. Use #{new_method}()" send(new_method) end deprecate :GetTitle, :title end
14) Around Alias
Around Alias uses the alias
keyword to store a copy of the original method under a new name, allowing you to redefine the original method name and to delegate off to the previous method implementation.
class String alias :orig_length :length # make alias of old method def length # define new method, override "Length of string '#{self}' is: #{orig_length}" end end "abc".length #=> "Length of string 'abc' is: 3"
15) Hook Methods
The method being called when event triggered, like Module#included
, Class#inherited
class String def self.inherited(subclass) puts "#{self} was inherited by #{subclass}" end end class MyString < String; end # => String was inherited by MyString
Method-related hooks:
- method_missing
- method_added
- method_removed
- method_undefined
- singleton_method_added
- singleton_method_removed
- singleton_method_undefined
Class & Module hooks:
- inherited
- included
- extended
- extend_object
- const_missing
- append_features
- initialize_copy
Marshalling hooks:
- marshal_dump
- marshal_load
16) Class Extension Mixin
Class Extension Mixin allows you to both include
and extend
a class
module MyMixin def self.included(base) # Hook Method base.extend ClassMethods end # Class Methods module ClassMethods def x puts "I'm X (a class method)" end end # Instance Methods def a puts "I'm A (an instance method)" end end class Foo include MyMixin end Foo.x # => I'm X (a class method) Foo.new.a # => I'm A (an instance method)