Object sits on Kernel on BasicObject¶
Level: 301 · a Ruby programmer who wants to know where puts comes from and what a "blank slate" object really is
One line: Object.ancestors is [Object, Kernel, BasicObject] and Object itself defines no public methods — every method a plain object has is Kernel's (including the private "functions" puts, p, require, raise and lambda) or BasicObject's eight (==, equal?, !, __send__, instance_eval and three more) — so a subclass of BasicObject cannot puts, raise, inspect or even name String without ::, which is exactly what makes it the right base for a proxy that forwards everything through method_missing.
There is no "global function" in Ruby. What looks like one — puts "x", require "json", raise "boom" — is a private instance method of the Kernel module, mixed into Object, called on whatever self happens to be. That is why a bare puts works inside any method of any class, why 5.method(:puts) exists, and why the top-level self (main) can call them. BasicObject is the root above all that: a class with eight public methods and five private hooks, whose subclasses start with nothing — no class, no inspect, not even Object's constants. The program measures the three layers, then builds one bare object and one forwarding proxy on BasicObject.
Verified output of basic_object_and_kernel_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
1. three things at the top
Object.ancestors [Object, Kernel, BasicObject]
Object.superclass BasicObject
BasicObject.superclass nil
Kernel.class Module
Object.instance_methods(false) []
Object.private_instance_methods(false).sort [:row, :section]
2. BasicObject: the smallest object there is
BasicObject.instance_methods.sort [:!, :!=, :==, :__id__, :__send__, :equal?, :instance_eval, :instance_exec]
BasicObject.private_instance_methods.sort [:initialize, :method_missing, :singleton_method_added, :singleton_method_removed, :singleton_method_undefined]
3. Kernel: the methods every object has
private, called without a receiver: puts p print require raise lambda proc loop rand format Integer exit
all in Kernel.private_instance_methods true
public: inspect to_s respond_to? is_a? class nil? send freeze dup instance_variable_get
all in Kernel.instance_methods true
method(:puts).owner Kernel
5.method(:puts).owner Kernel
Kernel.singleton_methods.include?(:puts) true
self.to_s / self.class (top level) main / Object
4. a BasicObject subclass has none of it
puts inside Bare NoMethodError: undefined method 'puts' for an instance of Bare
raise inside Bare NoMethodError: undefined method 'raise' for an instance of Bare
String inside Bare NameError: uninitialized constant Bare::String
(::Kernel.puts works from inside Bare)
::String.name inside Bare String
bare.inspect NoMethodError: undefined method 'inspect' for an instance of Bare
bare.class NoMethodError
p bare (p calls inspect) NoMethodError
bare == Bare.new false
bare.equal?(bare) true
!bare false
bare.instance_eval { @x = 1; @x } 1
BasicObject === bare / Object === bare true / false
5. a proxy: method_missing forwards everything, even class
(forwarding sort)
pr.sort [1, 2, 3]
(forwarding size)
pr.size 3
(forwarding class)
pr.class Array
(forwarding inspect)
pr.inspect [3, 1, 2]
(forwarding respond_to?)
pr.respond_to?(:sort) true
Proxy.instance_methods(false) [:method_missing]
Proxy.ancestors [Proxy, BasicObject]
Reading the output¶
- 1.
BasicObjecthas no superclass (nil);Object's isBasicObject, with the moduleKernelbetween them.Object.instance_methods(false)is empty:Objectadds no public methods of its own. Its private list holds:rowand:section— the two helpers this very program defined with a top-leveldef. A top-leveldefdefines a private instance method ofObject, which is why every object, and every class body, can call such a helper without a receiver. - 2. The whole of
BasicObject: eight public methods —!,!=,==,__id__,__send__,equal?,instance_eval,instance_exec— and five private ones, all hooks:initialize,method_missingand the threesingleton_method_*callbacks. - 3. The "functions" are private instance methods of
Kernel; the everyday methods —inspect,to_s,respond_to?,is_a?,class,nil?,send,freeze,dup— are its public ones.method(:puts).ownerisKernel, and so is5.method(:puts).owner: an Integer hasputs, privately.Kernel.singleton_methodsalso includesputs, because these are module functions — callable asKernel.putswith an explicit receiver, which row 4 uses. At the top levelselfismain, an instance ofObject. - 4. Inside
Bare < BasicObject,putsandraiseareNoMethodErrors andStringisNameError: uninitialized constant Bare::String— constant lookup walks the ancestors, andObject, where the core constants live, is not one of them. The escape hatch is a leading:::::Kernel.putsand::Stringwork. From outside,bare.inspect,bare.classandp bareall fail (pcallsinspect). What does work is whatBasicObjectowns:==(identity, so a secondBare.newis not equal),equal?,!andinstance_eval.Object === bareisfalse: a bare object is not anObject. - 5. A proxy on
BasicObjectwith amethod_missingforwards everything, because almost nothing is defined:sort,size,inspect,respond_to?— andclass, sopr.classanswersArray. On anObject-based proxy those four would be answered byKerneland never reachmethod_missing.Proxy.ancestorsis just[Proxy, BasicObject].method_missingandrespond_to_missing?has the rest of the technique, and building a DSL is where it is used.
Compared with Python¶
Python's root is object, and the differences start there. Row 1–2: object is not small — dir(object) lists 24 names, and they are its own (vars(object) holds them), where Ruby's Object owns nothing and borrows from Kernel. Row 3: builtins is Python's Kernel, but it is a module every scope sees, not a mixin every object carries — print is builtins.print and print.__module__ is builtins, while hasattr(5, 'print') is False, where Ruby's 5.method(:puts) exists. Row 4 is the structural difference: there is no class below object. Every class has it as a base, del object.__repr__ is a TypeError (the type is immutable), and setting a class's __bases__ to () is a TypeError, so no Python class can be a blank slate: Bare() has __repr__, __eq__, __hash__ and the rest whether it wants them or not. Row 5: a proxy uses __getattr__, which runs only when normal lookup fails, so pr.copy() is forwarded — but implicit special-method calls look on the type, not the instance, so len(pr) is a TypeError even though the explicit pr.__len__() is forwarded fine; and pr.__class__ and repr(pr) stay the proxy's own, where Ruby's proxy forwarded class and inspect. __getattribute__ intercepts every lookup, hit or miss, which is closer to method_missing's reach, and then has to fetch its own state through object.__getattribute__.
Verified output of basic_object_and_kernel_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. three things at the top
[c.__name__ for c in Plain.__mro__] ['Plain', 'object']
[c.__name__ for c in int.__mro__] ['int', 'object']
object.__bases__ ()
type(object).__name__ type
type(builtins).__name__ module
len(vars(object)) > 20 (object has its own methods) True
2. object: the root, and not small
len(dir(object)) 24
sorted(dir(object)) __class__ __delattr__ __dir__ __doc__ __eq__ __format__
__ge__ __getattribute__ __getstate__ __gt__ __hash__ __init__
__init_subclass__ __le__ __lt__ __ne__ __new__ __reduce__
__reduce_ex__ __repr__ __setattr__ __sizeof__ __str__ __subclasshook__
3. builtins: a module every scope sees, not methods every object has
functions: print repr input open format int isinstance len exit
all in vars(builtins) True
methods: __repr__ __str__ __eq__ __hash__ __class__ __getattribute__ __reduce__ __dir__
all in dir(object) True
print is builtins.print True
print.__module__ builtins
hasattr(5, 'print') False
hasattr(object(), 'print') False
4. there is no class below object
Bare.__bases__[0] is object True
hasattr(Bare(), '__repr__') True
repr(Bare()).startswith('<__main__.Bare object at 0x') True
del object.__repr__ TypeError
Rootless.__bases__ = () TypeError
bare == Bare() False
bare is bare True
not bare False
isinstance(bare, object) True
5. a proxy: __getattr__ forwards the misses, but not the dunders
(forwarding copy)
pr.copy() [3, 1, 2]
(forwarding __len__)
pr.__len__() (explicit call: forwarded) 3
len(pr) (implicit lookup skips __getattr__) TypeError
pr.__class__.__name__ Proxy
repr(pr).startswith('<__main__.Proxy object') True
isinstance(pr, list) False
non-dunder names in vars(Proxy) []
(intercepting append)
object.__getattribute__(st, '_target') [1, 2]
Try it¶
- Write a
Recorder < BasicObjectthat wraps a target, forwards every call throughmethod_missing, and appends each method name to a list reachable as__calls. Wrap an Array, callsize,first,sort,class,nil?,inspectandmap(&:upcase), then print the recorded names. Which of those calls would not have been recorded on anObject-based recorder, and why wasequal?not recorded? - Give the
Proxyfrom the program its ownclassandinspect(sop proxyprints#<Proxy of Array>andproxy.classisProxy) while still forwarding everything else, and add arespond_to?that asks the target. Check withproxy.respond_to?(:sort)andproxy.respond_to?(:nope).
Solution to 1
basic_object_and_kernel_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.
# Exercise 1: a Recorder proxy on BasicObject. It forwards every call to the
# target and keeps the names, and because BasicObject has no `class`, `nil?`
# or `inspect`, those calls are recorded and forwarded too.
class Recorder < BasicObject
def initialize(target)
@target = target
@calls = []
end
def __calls = @calls
def method_missing(name, *args, &blk)
@calls << name
@target.__send__(name, *args, &blk)
end
end
rec = Recorder.new(%w[pear apple fig])
puts "rec.size #{rec.size}"
puts "rec.first #{rec.first}"
puts "rec.sort #{rec.sort.inspect}"
puts "rec.class #{rec.class}"
puts "rec.nil? #{rec.nil?}"
puts "rec.inspect #{rec.inspect}"
puts "rec.map(&:upcase) #{rec.map(&:upcase).inspect}"
puts "recorded #{rec.__calls.inspect}"
puts "rec.equal?(rec) #{rec.equal?(rec)} (equal? is BasicObject's own, so it was not recorded)"
Verified output of basic_object_and_kernel_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
rec.size 3
rec.first pear
rec.sort ["apple", "fig", "pear"]
rec.class Array
rec.nil? false
rec.inspect ["pear", "apple", "fig"]
rec.map(&:upcase) ["PEAR", "APPLE", "FIG"]
recorded [:size, :first, :sort, :class, :nil?, :inspect, :map]
rec.equal?(rec) true (equal? is BasicObject's own, so it was not recorded)
See also¶
- Everything is an object —
Integer.ancestorsand the first look atKernelandBasicObject method_missingandrespond_to_missing?— the forwarding technique row 5 uses- Building a DSL — where a
BasicObjectproxy earns its keep putscallsto_s,pcallsinspect—to_s,inspectandpareKernel's, which is why a bare object has none- Every object has a class of its own — the singleton class sits in front of this chain
- Method lookup and
super— how a call walksancestorsand ends inmethod_missing - Constants and lexical scope — why
Stringis aNameErrorinsideBareand::Stringis not - Ruby docs:
BasicObject↗ — the eight methods and the proxy example - Ruby docs:
Kernel↗ — every "function" Ruby has - Python docs:
builtins↗ — the module that playsKernel's part - Python docs:
__getattr__and__getattribute__↗ — and the note on special method lookup - Refinements are lexical, not global —
refine String do … endinside a module builds aRefinement, andusingthat module activates it from…