Introduction
In Ruby, methods are not just syntax — they can be captured as first-class objects, stored in variables, passed to other methods, and introspected for metadata like arity and source location. This lesson explores the Method and UnboundMethod classes.
Key Concepts
- Method object: A bound reference to a method on a specific receiver, obtained with
obj.method(:name). - UnboundMethod: A method reference without a receiver, obtained with
Class.instance_method(:name). Must be bound before calling. - arity: The number of required arguments a method accepts.
Real World Context
Debugging tools like Pry use Method#source_location to show you exactly where a method is defined. When you type show-source User#save in Pry, it grabs the Method object, reads its source location, and opens the file. Understanding method objects lets you build similar introspection tools.
Deep Dive
Ruby lets you capture methods as objects and pass them around:
rubyclass Calculator def add(a, b) a + b end end calc = Calculator.new m = calc.method(:add) # Get Method object m.class # => Method m.call(2, 3) # => 5 m.arity # => 2 (number of required arguments) m.owner # => Calculator m.receiver # => calc
The Method object m is bound to calc, meaning you can call it later without referencing calc again. The owner tells you which class defined the method, and receiver tells you which object it is bound to.
You can also get an unbound method that is not tied to any specific instance:
rubyclass Calculator def add(a, b); a + b; end end # Get unbound method (no receiver) um = Calculator.instance_method(:add) um.class # => UnboundMethod # Bind to an instance to call it calc = Calculator.new bound = um.bind(calc) bound.call(2, 3) # => 5
An UnboundMethod cannot be called directly — you must bind it to an instance of the class (or a subclass) first. This is useful for transplanting methods between objects.
Method objects have several practical applications:
ruby# Pass method as block def double(n); n * 2; end [1, 2, 3].map(&method(:double)) # => [2, 4, 6] # Store methods for later actions = { add: method(:add), sub: method(:sub) } actions[:add].call(1, 2) # Find where a method comes from "hello".method(:upcase).source_location # => nil (C method) or ["file.rb", 42, 2, 44, 5] in Ruby 4.0
The &method(:double) pattern converts a method into a block, making it composable with iterators like map. source_location returns nil for C-implemented methods and a five-element array [file, start_line, start_col, end_line, end_col] for Ruby-defined methods (Ruby 4.0+; previously two elements).
You can also introspect a method's parameters:
rubym = "hello".method(:gsub) m.parameters # => [[:req, :pattern], [:opt, :replacement]] m.super_method # => Method for superclass version (if any)
The parameters method returns an array describing each parameter's type (:req, :opt, :rest, :keyreq, :key, :keyrest, :block) and name.
Common Pitfalls
- Calling an UnboundMethod directly —
UnboundMethod#calldoes not exist. You must bind it to an instance first with.bind(obj), or you will get aTypeError. - Assuming source_location always works — Methods implemented in C (like
String#upcase) returnnilforsource_location. Check fornilbefore using the result.
Best Practices
- Use
&method(:name)for clean functional style — Instead of[1,2,3].map { |n| double(n) }, use[1,2,3].map(&method(:double))for concise, point-free code. - Use
source_locationfor debugging — When you cannot figure out where a method comes from (especially in metaprogrammed code),obj.method(:name).source_locationis your best friend.
Summary
obj.method(:name)returns a boundMethodobject you can call, store, and introspect.Class.instance_method(:name)returns anUnboundMethodthat must be bound before use.- Method objects expose metadata like
arity,parameters,owner, andsource_location(which returns 5 elements in Ruby 4.0: file, start_line, start_col, end_line, end_col).
Code Examples
class Greeter
def hello(name)
"Hello, #{name}!"
end
end
m = Greeter.new.method(:hello)
m.call("Ruby") # => "Hello, Ruby!"
m.arity # => 1
m.owner # => Greeter
m.parameters # => [[:req, :name]]
["Alice", "Bob"].map(&m) # => ["Hello, Alice!", "Hello, Bob!"]