/ Tags: RUBY 3 / Categories: RUBY

Method Objects in Ruby — Treating Methods as First-Class Values

Ruby methods are first-class citizens in a way that most developers don’t fully use. You can grab a method as an object, pass it around, convert it to a proc, unbind it from its receiver, and rebind it to a different object. This isn’t academic — method(:name) is one of the cleanest ways to pass behavior without blocks, and UnboundMethod is the key to some of Ruby’s most expressive patterns. Understanding the Method and UnboundMethod classes opens composition patterns that lambdas and Procs can’t match.

method() — Getting a Method as an Object


The method method (called on any object) returns a Method object representing the named method, bound to that object.

Example:

# Calling method on a string
str = "hello world"
m   = str.method(:upcase)

m.class   # => Method
m.call    # => "HELLO WORLD"
m.()      # => "HELLO WORLD"  (shorthand call syntax)
m.owner   # => String
m.arity   # => 0

Example:

# Method objects respond to the same call interface as lambdas
double = method(:puts)
[1, 2, 3].each(&double)
# 1
# 2
# 3

# vs the block equivalent
[1, 2, 3].each { |n| puts n }

&method(:name) converts the Method to a proc and passes it as a block. This is the idiomatic pattern for passing named methods to iterators without writing a block.

Using method() With Iterators


The &method(:name) pattern reads clearly and avoids noise when the operation has a descriptive name.

Example:

# Parsing input
["1", "2", "3"].map(&method(:Integer))   # => [1, 2, 3]
["1.5", "2.7"].map(&method(:Float))      # => [1.5, 2.7]

# Method reference on a class
User.where(active: true).map(&method(:process_user))

# Composing with built-in methods
words = ["hello", "world", "ruby"]
words.map(&:upcase).select(&method(:long_word?))

def long_word?(w) = w.length > 4

# => ["HELLO", "WORLD"]  (words over 4 chars, uppercased)

Example:

# Instance method on a specific object
formatter = NumberFormatter.new(locale: :en_US)
prices     = [9.99, 14.50, 299.00]

prices.map(&formatter.method(:format))
# => ["$9.99", "$14.50", "$299.00"]

# vs the block version
prices.map { |p| formatter.format(p) }

Both work. The method version is cleaner when the formatter is reused across multiple collections or when composing with other method references.

Composition with >> and <<


Ruby 2.6 added >> and << operators to Method and Proc for function composition.

Example:

double  = ->(n) { n * 2 }
add_one = ->(n) { n + 1 }

# >> composes left-to-right: double first, then add_one
double_then_add = double >> add_one
double_then_add.call(5)   # => 11  (5*2 = 10, 10+1 = 11)

# << composes right-to-left: add_one first, then double
add_then_double = double << add_one
add_then_double.call(5)   # => 12  (5+1 = 6, 6*2 = 12)

Example:

# Composing method objects in a pipeline
normalize  = method(:normalize_string)   # strips, downcases
validate   = method(:validate_email)     # returns true/false
process    = normalize >> validate

emails = [" [email protected] ", "invalid", "[email protected]"]
emails.filter_map { |e| e if process.call(e) }
# => ["[email protected]", "[email protected]"]  (normalized and validated)

Composition is clearest when each step is a named operation. Anonymous lambdas that do non-obvious things are harder to read than named methods composed in sequence.

UnboundMethod — Method Without a Receiver


An UnboundMethod is a method that’s been detached from its instance. You obtain one with instance_method, and bind it to a specific instance with bind.

Example:

# Get an unbound method from a class
unbound = String.instance_method(:upcase)
unbound.class   # => UnboundMethod
unbound.owner   # => String

# Bind to a specific instance and call
bound = unbound.bind("hello")
bound.call   # => "HELLO"

# The bind must be compatible — same class or subclass
class MyString < String; end

unbound.bind(MyString.new("hello")).call  # => "HELLO" — subclass works
unbound.bind(42)  # => TypeError: bind argument must be an instance of String

Example:

# Classic use: calling a method from a specific module in the hierarchy
module Greetable
  def greet
    "Hello, I'm #{name}"
  end
end

class Person
  include Greetable
  attr_reader :name

  def initialize(name)
    @name = name
  end

  def greet
    "#{super} (overridden)"  # super works, but what if you need the module version specifically?
  end
end

# Extract and call the module's version directly
module_greet = Greetable.instance_method(:greet)
ada = Person.new("Ada")
module_greet.bind(ada).call   # => "Hello, I'm Ada"  (module version, not overridden)

Method Introspection


Method objects expose metadata that’s useful for tooling, debugging, and testing.

Example:

m = "hello".method(:gsub)

m.arity      # => -2  (negative = variable arity; formula: -(required + 1))
m.owner      # => String
m.receiver   # => "hello"
m.name       # => :gsub
m.original_name  # => :gsub  (differs if aliased)

# Source location — where is this method defined?
User.instance_method(:authenticate).source_location
# => ["/app/models/user.rb", 42]

# Works on MRI for Ruby-defined methods; returns nil for C-implemented methods
"hello".method(:upcase).source_location  # => nil  (C method)

Example:

# Checking if an object responds to a method
user = User.new
user.respond_to?(:authenticate)      # => true
user.respond_to?(:authenticate, true) # => true for private methods too

# Listing methods defined at a specific level
User.public_instance_methods(false)   # defined on User directly (false = not inherited)
User.private_instance_methods(false)
User.protected_instance_methods(false)

Pro-Tip: When building data transformation pipelines, prefer method references over anonymous lambdas for named operations. data.map(&method(:normalize)).select(&method(:valid?)).map(&method(:format)) reads like a prose description of the pipeline. Anonymous lambdas in a chain push all the logic inline where it competes for attention with the pipeline structure itself. The named method version also makes each step independently testable — normalize, valid?, and format are real methods you can test in isolation.

Conclusion


Method and UnboundMethod give you the ability to treat named behavior as values — pass methods to iterators, compose pipelines, extract behavior from modules, and inspect where code lives. The &method(:name) pattern is the most commonly useful form, turning any named method into a block-compatible callable with no lambda syntax needed. Composition with >> makes pipelines of named transformations expressive and testable in a way that nested lambdas can’t match.

FAQs


Q1: What’s the difference between method(:foo) and proc { foo() }?
method(:foo) binds to the current receiver and has lambda-like arity checking. proc { foo() } is a closure that captures the current scope and has proc-like argument leniency. Method objects are better when you want to pass a named operation without defining a new anonymous callable.

Q2: Can I store and call Method objects across different objects?
A bound Method is tied to its receiver. If the receiver is garbage-collected, the method object is invalid. Use UnboundMethod when you need a method detached from any instance, then bind to the target instance at call time.

Q3: Does method work with private methods?
No — method only returns public methods. To get a private method as an object, use send(:method_name) at the call site, or use UnboundMethod with careful access. This is intentional — it enforces the public interface.

Q4: What does arity return for methods with optional arguments?
Negative values: -(required_count + 1). A method with 2 required and 1 optional returns -3. A method with 0 required and 1 optional returns -1. The formula: if arity is negative, required count is (-arity - 1).

Q5: Can >> compose methods with different arities?
The composed method’s arity matches the leftmost callable. Each callable in the chain must accept the output of the previous. If types or argument counts don’t match, you’ll get a runtime error at call time, not at composition time.

cdrrazan

Rajan Bhattarai

Full Stack Software Developer! 💻 🏡 Grad. Student, MCS. 🎓 Class of '23. GitKraken Ambassador 🇳🇵 2021/22. Works with Ruby / Rails. Photography when no coding. Also tweets a lot at TW / @cdrrazan!

Read More