Metaprogramming 101 with ruby
09 Jan 2019Metaprogramming is writing code that writes code
One of the most impressive aspects of Ruby is its metaprogramming capabilities.
Being a dynamic language, Ruby gives you the freedom to define classes and methods at runtime! Awesome!
Using Metaprogramming, we can reopen and modify classes, catch methods that do not exist, write code that is DRY and many more.
A problem may arise with opening a class at runtime and adding a method when the method created already exists is overwritten by the new definition. This is not intended! This process of editing classes in ruby is called Monkeypatching.
In Ruby, everything is an object.
Ruby Object Model
+---------------+
| |
| BasicObject |
| |
+---------------+
^
|
| Superclass
|
+---------------+ +---------------+
| | Superclass | |
| Object | <------------------------ | Module |
| | -------------- | |
+---------------+ | +---------------+
^ | Class ^
| ---------------- |
| Superclass | | Superclass
| \ / |
+---------------+ +---------------+
| | Class | |
| MyClass | -------------------------> | Class |<------
| | | | |
----------------+ +---------------+ |
^ | |
| |______________|
| Class Class
|
+---------------+
| |
| obj |
| |
+---------------+
class MyClass
@a
def set(a)
@a = a
end
def get
@a
end
end
obj = MyClass.new
# Ancestor Chain
p obj.class # MyClass
p obj.class.class # Class
p obj.class.class.superclass # Module
p obj.class.class.superclass.superclass # Object
p obj.class.superclass # Object
p obj.class.superclass.superclass # BasicObject - The absolute parent of every object in Ruby.
Methods
Dynamically Defining Methods
Consider the following snippet
def a
puts "in a"
end
def b
puts "in b"
end
def c
puts "in c"
end
a
b
c
# => in a
# => in b
# => in c
We can remove the above redundancy by using Metaprogramming
%w(a b c).each do |s|
define_method(s) do
puts "in #{s}"
end
end
a
b
c
# => in a
# => in b
# => in c
You can find more about Module#define_method here.
The ActiveRecord code base is a prime example of how you can use metaprogramming to the max.
Dynamically Calling Methods
Dynamically calling methods or attributes is a form of reflective property.
An example of how to call a method by either the string or symbol name of that method in ruby:
%w(a1 a2 a3).each do |s|
define_method(s) do
puts "#{s} was called"
end
end
(1..3).each { |n| send("a#{n}") }
# => a1 was called
# => a2 was called
# => a3 was called
The Object#send method is how we can dynamically call methods.
Because every object in Ruby inherits from Object, you can also call send as a method on any object to access one of its other methods or attributes. Object#send
even allows you to call private methods! Maybe you want to use Object#public_send.
Ghost Methods
What if I call a method that does not exists? Surely a NoMethodError
would be thrown. We can avoid this error by using BasicObject#method_missing .
class A
def method_missing(method, *args, &block)
puts "You called: #{method}(#{args.join(', ')})"
end
end
a = A.new
a.alphabet
a.alphabet('a', 'b') { "foo" }
# => You called: alphabet()
# => You called alphabet(a, b)
# => (You also passed it a block)
It takes extra time to hit the method_missing handler because you traverse the Ancestor Chain.
A few more powerful concepts that you can add to your ruby arsenal.
Closures
Scope in ruby shifts at 3 major spots:
- Module Definition
- Class Definition
- Methods
Something like this is impossible
v = "Hello"
class A
# print v here
def hello
# and here
end
end
This can be achieved by defining class and methods dynamically.
v = "Hello"
my_class = Class.new do
"#{v} in class definition"
define_method :hello do
"#{v} in method definition"
end
end
puts my_class.new.hello
# => Hello in method definition
This seemingly “scopeless” process is called a Flat Scope.
Evals
In ruby, there are 3 main types of evals:
- Instance Eval
- Class Eval
- Eval