Skip to content

A method is a value once you ask for it

Level: 301 · someone who has used send and define_method and wants the objects behind them

One line: obj.method(:name) hands back a Method bound to obj, and Klass.instance_method(:name) an UnboundMethod you can bind to any instance of Klass later — Python has bound methods too, but Cls.f is a plain function and types.MethodType binds it to anything without checking.

A Ruby method is not an object while it sits in a class; it becomes one when you ask. obj.method(:name) looks the name up the way a call would and wraps what it finds in a Method that remembers three things: the code, the object it was found on (receiver) and the class or module that defined it (owner). Call it with call, with .() or with [], all three are the same call. arity is negative when parameters are optional (-2 means one required and at least one more allowed), and parameters lists every parameter with its kind, which is what a library uses to decide how to call something it was handed.

unbind takes the receiver away and leaves an UnboundMethod, the same object Klass.instance_method(:name) returns directly. Binding it back is type-checked: bind accepts only an instance of the owner or one of its subclasses and raises TypeError for anything else. When the instance belongs to a subclass that overrides the method, the owner's version runs anyway — binding is a way to reach past an override. bind_call(obj, *args) binds and calls without the intermediate object. super_method walks up one step of the same lookup super would take, original_name sees through an alias, to_proc produces a lambda so &obj.method(:name) slots a method into map, and method(:puts) on main shows that even the top-level printing functions are methods with an owner (Kernel).

Verified output of method_objects_and_unbound_methods_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

 1. ada.method(:greet).class                             Method
 2. call it: m.call("Hi"), m.("Hi"), m["Hi"]             ["Hi, Ada!", "Hi, Ada!", "Hi, Ada!"]
 3. m.name, m.owner                                      [:greet, Greeter]
 4. m.receiver, m.receiver.equal?(ada)                   [#<Greeter Ada>, true]
 5. m.arity, m.parameters                                [-2, [[:req, :greeting], [:opt, :punct]]]
 6. m.unbind.class, == Greeter.instance_method(:greet)   [UnboundMethod, true]
 7. u.bind(bob).call("Yo"), u.bind_call(bob, "Yo")       ["Yo, Bob!", "Yo, Bob!"]
 8. u.bind("a string") raises                            TypeError
 9. Greeter's greet bound to a Loud (a subclass) instance "Hi, Eve!"
10. eve.method(:greet).owner, .super_method.owner        [Loud, Greeter]
11. eve.method(:greet).super_method.call("Hi")           "Hi, Eve!"
12. ...super_method.super_method (nothing above)         nil
13. m.to_proc.class, m.to_proc.lambda?                   [Proc, true]
14. %w[Hi Yo].map(&ada.method(:greet))                   ["Hi, Ada!", "Yo, Ada!"]
15. ada.method(:hello).name, .original_name              [:hello, :greet]
16. m == ada.method(:greet), m.equal?(ada.method(:greet)) [true, false]
17. method(:puts).owner, method(:puts).receiver          [Kernel, main]
18. 1.method(:+).owner, 1.method(:+).call(2)             [Integer, 3]
19. [1, 2, 3].map(&1.method(:+))                         [2, 3, 4]

Reading the output

Rows 1–5 are the bound Method: its class, the three call spellings, name/owner, receiver (the very object, equal? is true) and arity/parameters (-2: one required, one optional). Row 6 unbinds it and shows that Greeter.instance_method(:greet) is an equal UnboundMethod. Rows 7–9 bind it again: to another Greeter, with bind(...).call and with bind_call; to a String, which is refused with TypeError; and to a Loud, a subclass that overrides greet — the output is Greeter's lower-case version, not Loud's upper-case one, because the unbound method decides which code runs, not the receiver's class.

Rows 10–12 walk the lookup chain by hand: eve.method(:greet) is owned by Loud, its super_method by Greeter, calling that gives the un-shouted greeting, and one more super_method is nil because nothing above Greeter defines greet. Rows 13–14 turn the method into a lambda-style Proc and pass it with &. Row 15: an alias keeps its own name but original_name is the method it was copied from. Row 16: every method(:greet) call allocates a new Method — equal by ==, never equal?. Rows 17–19: puts is owned by Kernel and its receiver is main; + on an Integer is a method like any other, so 1.method(:+) maps over an array.

Compared with Python

Python's obj.f is also created on access: row 16 shows m == ada.greet is True while m is ada.greet is False, the same pair as Ruby. A bound method carries __self__ (the receiver, row 4) and __func__ (row 6), and __func__ is Greeter.greet because in Python 3 the class attribute is the plain function itself — there is no unbound-method type, so there is nothing to unbind (row 6 prints function). Where Ruby type-checks bind, Python checks nothing: Greeter.greet("a string", "Yo") runs until the body asks the string for .name and fails with AttributeError (row 8), and types.MethodType(func, obj) binds a function to any object at all (row 7). Calling Greeter.greet(eve, ...) on a Loud instance reaches past the override exactly as Ruby's row 9 does.

super(Loud, eve).greet is the counterpart of super_method (rows 10–11), but at the top of the chain it raises AttributeError instead of returning None (row 12). An alias is the same function object (row 15, is is True), so Python has no original_name to recover. inspect.signature stands in for parameters (row 5) and reports kinds by name; there is no arity because a signature is a richer object. print is a builtin_function_or_method with a module and no owner class (row 17), and (1).__add__ is a method-wrapper around the C slot (row 18) — Python spells "operators are methods" through dunder names, Ruby through 1.method(:+).

Verified output of method_objects_and_unbound_methods_py.py — regenerated by tools/run_examples.py, never hand-typed.

 1. type(ada.greet).__name__                             'method'
 2. call it: m("Hi") -- one spelling                     ['Hi, Ada!']
 3. m.__name__, m.__qualname__                           ['greet', 'Greeter.greet']
 4. m.__self__, m.__self__ is ada                        [<Greeter Ada>, True]
 5. signature(m), parameter kinds                        ["(greeting, punct='!')", ['POSITIONAL_OR_KEYWORD', 'POSITIONAL_OR_KEYWORD']]
 6. type(Greeter.greet).__name__, m.__func__ is Greeter.greet ['function', True]
 7. Greeter.greet(bob, "Yo"), MethodType(u, bob)("Yo")   ['Yo, Bob!', 'Yo, Bob!']
 8. Greeter.greet("a string", "Yo") raises               'AttributeError'
 9. Greeter's greet called on a Loud (a subclass) instance 'Hi, Eve!'
10. eve.greet.__func__.__qualname__, super's             ['Loud.greet', 'Greeter.greet']
11. super(Loud, eve).greet("Hi")                         'Hi, Eve!'
12. super(Greeter, eve).greet (nothing above) raises     'AttributeError'
13. callable(m), ismethod(m), isfunction(m)              [True, True, False]
14. list(map(ada.greet, ["Hi", "Yo"]))                   ['Hi, Ada!', 'Yo, Ada!']
15. Greeter.hello.__name__, Greeter.hello is Greeter.greet ['greet', True]
16. m == ada.greet, m is ada.greet                       [True, False]
17. type(print).__name__, print.__module__               ['builtin_function_or_method', 'builtins']
18. type((1).__add__).__name__, (1).__add__(2)           ['method-wrapper', 3]
19. list(map((1).__add__, [1, 2, 3]))                    [2, 3, 4]

Try it

  1. Write describe(m) that takes a Method or an UnboundMethod and returns one line with its name, its owner, how many required and how many optional positional parameters it has, and whether it is bound (and to what). Run it on Greeter.new("Ada").method(:greet), Greeter.instance_method(:greet), 1.method(:+), method(:puts) and Array.instance_method(:push).
  2. Define a class whose inspect hides the object's state, then call the original Object#inspect on an instance with Object.instance_method(:inspect).bind_call(obj). Print whether the original result starts with #< (do not print it whole — it contains an address) and the class's own inspect. Then time bind(obj).call against bind_call(obj) in a loop of a million calls and explain the difference you see.
Solution to 1

method_objects_and_unbound_methods_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.

# Kata: describe any Method or UnboundMethod in one line -- its name, its
# owner, how many required and optional positional parameters it takes,
# and whether it is bound to a receiver.

class Greeter
  def initialize(name)
    @name = name
  end

  def greet(greeting, punct = "!")
    "#{greeting}, #{@name}#{punct}"
  end

  def inspect
    "#<Greeter #{@name}>"
  end
end

def describe(m)
  kinds = m.parameters.map(&:first)
  required = kinds.count(:req)
  optional = kinds.count(:opt)
  bound = m.respond_to?(:receiver) ? "bound to #{m.receiver.inspect}" : "unbound"
  format("%-6s from %-8s required=%d optional=%d  %s", m.name, m.owner, required, optional, bound)
end

puts describe(Greeter.new("Ada").method(:greet))
puts describe(Greeter.instance_method(:greet))
puts describe(1.method(:+))
puts describe(method(:puts))
puts describe(Array.instance_method(:push))

Verified output of method_objects_and_unbound_methods_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

greet  from Greeter  required=1 optional=1  bound to #<Greeter Ada>
greet  from Greeter  required=1 optional=1  unbound
+      from Integer  required=1 optional=0  bound to 1
puts   from Kernel   required=0 optional=0  bound to main
push   from Array    required=0 optional=0  unbound

See also