Skip to content

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. BasicObject has no superclass (nil); Object's is BasicObject, with the module Kernel between them. Object.instance_methods(false) is empty: Object adds no public methods of its own. Its private list holds :row and :section — the two helpers this very program defined with a top-level def. A top-level def defines a private instance method of Object, 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_missing and the three singleton_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).owner is Kernel, and so is 5.method(:puts).owner: an Integer has puts, privately. Kernel.singleton_methods also includes puts, because these are module functions — callable as Kernel.puts with an explicit receiver, which row 4 uses. At the top level self is main, an instance of Object.
  • 4. Inside Bare < BasicObject, puts and raise are NoMethodErrors and String is NameError: uninitialized constant Bare::String — constant lookup walks the ancestors, and Object, where the core constants live, is not one of them. The escape hatch is a leading ::: ::Kernel.puts and ::String work. From outside, bare.inspect, bare.class and p bare all fail (p calls inspect). What does work is what BasicObject owns: == (identity, so a second Bare.new is not equal), equal?, ! and instance_eval. Object === bare is false: a bare object is not an Object.
  • 5. A proxy on BasicObject with a method_missing forwards everything, because almost nothing is defined: sort, size, inspect, respond_to? — and class, so pr.class answers Array. On an Object-based proxy those four would be answered by Kernel and never reach method_missing. Proxy.ancestors is just [Proxy, BasicObject]. method_missing and respond_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

  1. Write a Recorder < BasicObject that wraps a target, forwards every call through method_missing, and appends each method name to a list reachable as __calls. Wrap an Array, call size, first, sort, class, nil?, inspect and map(&:upcase), then print the recorded names. Which of those calls would not have been recorded on an Object-based recorder, and why was equal? not recorded?
  2. Give the Proxy from the program its own class and inspect (so p proxy prints #<Proxy of Array> and proxy.class is Proxy) while still forwarding everything else, and add a respond_to? that asks the target. Check with proxy.respond_to?(:sort) and proxy.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