Concept of metaclass Metaprogramming in ruby -2
You can write the code that generates the code!
To understand the concept of metaclass we’ll first take a look at what is instance method and what is class method.
Instance method and Class method
In Ruby, a class is an object that defines a blueprint to create other objects. Classes define which methods are available on any instance of that class.
Whenever there’s a method defined in a class (without the self. prefix) it will create an instance method on that class. so that, any instance of that class can access that method.
class User
def initialize(name)
@name = name
end
# instance method
def name
@name
end
end
user = User.new('harish')
user.name # => "harish"
In the above code i have created a class named User and defined a instance method name, and then i have created a instance user with the help of User.new. So the ‘user’ instance now can access the method ‘name’. Now this instance method is stored in the virtual method table of the User class, and any instance of that class will refer to that method table to access the instance methods.
we have created the instance method now let’s create the class method
class User
def initialize(name)
@name = name
end
# instance method
def name
@name
end
# class method
def self.all
[new('pratik'), new('ashmita'), new('milan')]
end
end
user = User.new('harish')
user.name # => "harish"
User.all
# => [#<User:0x0000562c550db920 @name="pratik">, #<User:0x0000562c550db8d0 @name="ashmita">, #<User:0x0000562c550db880 @name="milan">]
In the above code we have added one more method with the prefix self. , we call it a class method and it can be accessed with the class_name.method , Now the class methods are not stored in the class’ method table, They are stored in the class’ metaclass.
What is metaclass?
We say that each object in ruby has it’s own class. Now apart from the class. Each object in ruby has it’s own metaclass, They are singletons meaning they belong to particular object. SO if we create multiple instances of the class then they will share the same class but all of them will have the different metaclass.
obj1, obj2, obj3 = User.all
# class of all the objects
[obj1.class, obj2.class, obj3.class] # => [User, User, User]
In the above code we have seen that the class of all the objects is same which is User.
How do we prove that each object is having a different metaclass?
we will revisit the concept of open class to get the metaclass of each object. But first let’s see what should we do to have the access to metaclass.
we’ll use the syntax class << self. through this syntax you are basically pointing the self to the metaclass of current class.
class User
self # => User (current class)
class << self
MY_META = self # => <Class:User>(metaclass of current class)
def my_meta
MY_META
end
end
end
User.my_meta # => <Class:User>
In the above code we saw the metaclass of User, Now we want the metaclass of every instance of the User. we’ll do some small changes to the above code.
class User
self # => User (current class)
class << self
MY_META = self # => <Class:User>(metaclass of current class)
def my_meta
MY_META
end
end
def print_my_metaclass
self # => user object
class << self
self # => the metaclass of current object
end
end
end
User.my_meta # => <Class:User>
obj1 = User.new
obj2 = User.new
[obj1.print_my_metaclass, obj2.print_my_metaclass]
# => [#<Class:#<User:0x0000564cc1662790>>, #<Class:#<User:0x0000564cc1662740>>]
In the above code we just proved that each object is having different metaclass. But we dont need to do this heck everytime to see the metaclass. Ruby provides us the method already to see the metaclass of the objects. which is singleton_class. So you can just do obj1.singleton_class.
What you can do by having an access to metaclass?
By having access to the metaclass of object, Ruby allows adding methods directly to existing objects. Doing so won’t add a new method to the object’s class.
class User
def initialize(name)
@name = name
end
def name
@name
end
end
user = User.new('pratik')
user2 = User.new('milan')
# method added to only 'user' object
def user.last_name
'vyas'
end
user.name # => pratik
user.last_name # => vyas
user2.last_name # => undefined method `last_name' for #<User:0x0000562ad3f2ace8 @name="milan">
user.singleton_class.instance_methods false # => [:last_name]
In the above code last_name
method is added as instance method to the metaclass of user object. Because When defining a method and passing a receiver, the new method is added to the receiver’s (user object) metaclass as instance method, instead of adding it to the class’ method table.
And same will happen here.
class User
def self.all
[new("pratik"), new("milan"), new("ashmita")]
end
end
User.singleton_class.instance_methods false # => [:all]
Now in the above code we have defined our receiver as self
so the all
method is added as instance method to the self’s
metaclass.
In the above concept we just proved that class methods are instance methods to the metaclass.
In the next article we’ll see how we can modify the class and objects on the fly with help of instance_eval and class_eval.