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¶
- Write
describe(m)that takes aMethodor anUnboundMethodand 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 onGreeter.new("Ada").method(:greet),Greeter.instance_method(:greet),1.method(:+),method(:puts)andArray.instance_method(:push). - Define a class whose
inspecthides the object's state, then call the originalObject#inspecton an instance withObject.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 owninspect. Then timebind(obj).callagainstbind_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.
See also¶
- A Binding is a scope you can carry around — the other reified piece of a method: its local variables and
self - TracePoint sees every call and return —
method_idanddefined_classon an event are the samenameandowneraMethodreports - Currying and composition —
Method#curry,>>and<<on the objects this page creates - Method lookup and
super— the chain thatownerandsuper_methodwalk by hand here sendandpublic_send— calling by name without reifying the method first- Introspection —
instance_methods,parametersandsource_locationacross a whole class - Operators are methods — why
1.method(:+)exists at all - Function pointers ↗ — the Rust library on a function as a value with no environment; a
Methodis the opposite, code plus the receiver it was found on Method↗ andUnboundMethod↗ — the reference forarity,parameters,bind_callandsuper_methodinspect— signatures and bound methods ↗ — Python'ssignature,ismethodandisfunction- Procs and lambdas differ in arity and
return— Every proc and lambda is aProc, but a lambda checks its argument count andreturns from itself, while…