Reflection in Ruby
Introspection, sometimes referred to as reflection, is fundamental to Ruby’s dynamic nature and is intimately associated with the metaprogramming strategies we just covered. The ability of a program to analyze its own state and structure is basically what is meant by reflection. At runtime, it enables a Ruby program to retrieve details about its elements (such as classes, objects, and methods) and their settings.
Ruby’s broad reflection API is mostly implemented using methods included in the Module, Object, and Kernel classes.
Key Uses of Reflection
Examining Program State and Structure: You can query the existence and properties of classes and methods by treating them as objects using reflection. This is particularly helpful for interactively investigating a new object or class hierarchy, like in troubleshooting.
Dynamic Interaction: Beyond simple inspection, the reflection API enables a program to dynamically change its structure and state by adding new classes and methods, setting variables, and calling methods by name.
Enforcing Contracts and Duck Typing: The idea of duck typing can be used to enforce interfaces or software contracts by using reflection to make sure an object implements specific needed functions.
Reflection Methods and Code Examples
Many techniques for inspecting objects, classes, and methods are available in Ruby.
Inspecting Object Types and Ancestry
With reflection methods, you may quickly find out an object’s type, class, and relationship to other classes.
| Method | Purpose | Source |
o.class | Returns the class of object o | |
c.superclass | Returns the parent class of class c | |
C.ancestors | Returns an array listing the ancestors (classes and modules) of class C | |
o.instance_of? c | Determines whether o is an instance of class c | |
o.kind_of? c or o.is_a? c | Checks if o is an instance of c, one of its subclasses, or if it includes module c |
Example (Class and Ancestry Introspection):
class A
def a; end
end
module B
def b; end
end
class C < A
include B
def c; end
end
# Print the results
puts "C.superclass:"
puts C.superclass
puts "\nC.ancestors:"
puts C.ancestors.inspect
puts "\nC.instance_methods(false):"
puts C.instance_methods(false).inspect
Output
C.superclass:
A
C.ancestors:
[C, B, A, Object, Kernel, BasicObject]
C.instance_methods(false):
[:c]
Listing and Testing Methods
The methods that an object or class implements can be dynamically found, which aids in the implementation of “duck typing” logic.
| Method | Purpose | Source |
o.methods | Returns an array of names (symbols/strings) of all public methods available to object o | |
o.respond_to? name | Determines whether o has a public or protected method with the specified name (pass true as the second argument to check private methods too) | |
C.instance_methods(false) | Returns public instance methods defined directly in class C, excluding inherited ones | |
o.singleton_methods | Returns the names of singleton methods defined specifically on object o |
Example (Method Listing and Checking):
s = "Hello"
puts "Class of s:"
puts s.class # => String
puts "\nDoes s respond to :upcase?"
puts s.respond_to?(:upcase) # => true
puts "\nDoes s respond to :undefined_method?"
puts s.respond_to?(:undefined_method) # => false
Output
Class of s:
String
Does s respond to :upcase?
true
Does s respond to :undefined_method?
false
Inspecting and Manipulating Variables
Direct variable inspection and manipulation are made possible via reflection; however, local variables typically necessitate the usage of eval with a Binding object.
| Method | Purpose | Source |
o.instance_variables | Returns an array of instance variable names (symbols/strings) for object o | |
o.instance_variable_get(symbol) | Returns the value of the named instance variable | |
o.instance_variable_set(symbol, value) | Sets the value of the named instance variable | |
o.instance_variable_defined?(:@name) | Checks if the specified instance variable has been defined on the object |
Example (Instance Variable Reflection):
class Foo
def initialize
@bar = 42
end
end
f = Foo.new
puts "Instance variables:"
p f.instance_variables # => [:@bar]
puts "\nGet value of @bar:"
p f.instance_variable_get(:@bar) # => 42
puts "\nSet @bar to 17:"
p f.instance_variable_set(:@bar, 17) # => 17
puts "\nIs @bar defined?"
p f.instance_variable_defined?(:@bar) # => true
puts "\nRemove @bar and return its value:"
p f.remove_instance_variable(:@bar) # => 17
puts "\nInstance variables after removal:"
p f.instance_variables # => []
Output
Instance variables:
[:@bar]
Get value of @bar:
42
Set @bar to 17:
17
Is @bar defined?
true
Remove @bar and return its value:
17
Instance variables after removal:
[]
Obtaining Method Objects and Dynamic Invocation
For dynamic execution and giving methods as arguments (such as callbacks), it is necessary that methods themselves be able to be regarded as data objects.
| Method | Purpose | Source |
o.method(:name) | Looks up the named method in object o and returns a callable Method object | |
M.instance_method(:name) | Returns an UnboundMethod object associated with method :name in module/class M | |
m.call(*args) | Invokes the method object m | |
o.send(symbol, *args) | Invokes the method identified by the symbol/name directly on object o |
Example (Dynamic Method Invocation using send): Using send allows you to call a method whose name is determined at runtime:
greeting = "hello"
method_name = :upcase
# Dynamically call the :upcase method
puts greeting.send(method_name) # => "HELLO"
# A method object can be stored and called later:
reverse_method = greeting.method(:reverse)
puts reverse_method.call # => "olleh"
Output
HELLO
olleh
Reflection gives Ruby developers a powerful microscope and a range of dynamic tools that allow them to examine and manipulate the program’s components in great detail. This serves as the basis for advanced metaprogramming design patterns.
