skip to content

In Ruby, a spec gives one object a singleton method — why can Marshal.dump of it then fail, and which objects refuse singleton methods entirely?

level: seniorimportance: should knowfreq 30%

answer

  1. singleton can't be dumped
  2. TypeError for Integer, Float, Symbol
  3. FrozenError on frozen objects
  4. nil means NilClass
  5. classes live the whole process

basics

~20 s

Marshal cannot serialize methods, so dumping an object whose singleton class holds methods raises TypeError. Integers, Floats and Symbols refuse singleton methods with TypeError, frozen objects with FrozenError, and nil, true and false patch their shared classes.

solid answer

~40 s

`Marshal.dump` writes an object's class name and data, but it has no way to write code, so if the object's singleton class contains methods (or instance variables), it raises `TypeError` with "singleton can't be dumped". Anything that marshals objects — a cache store, a copy made by dumping and loading, passing objects between processes — then breaks for that one object. Some receivers refuse singleton methods outright: Integers, Floats and Symbols raise `TypeError` ("can't define singleton"), and a frozen object raises `FrozenError`, because its singleton class is frozen with it (an interned frozen string raises `TypeError` instead). `nil`, `true` and `false` accept the `def` but put the method on `NilClass`, `TrueClass` or `FalseClass`, so it applies everywhere. Singleton methods on long-lived objects, including classes, last until the process exits.

code

ruby · 16 lines
ruby
class Gateway; end

gateway = Gateway.new
def gateway.charge(amount) = :approved

Marshal.dump(Gateway.new)       # works: no singleton methods
Marshal.dump(gateway)           # TypeError: singleton can't be dumped

code = :ok
def code.x = 1                  # TypeError: can't define singleton

frozen = Gateway.new.freeze
def frozen.x = 1                # FrozenError

def nil.charge(amount) = :oops  # defines NilClass#charge for every nil
nil.charge(1)                   # => :oops

go deeper

for a junior

Recall that a method added with def obj.x stays on that object and that numbers and symbols cannot take one.

for a middle

Explain why Marshal cannot dump singleton methods, why frozen objects refuse them, and why def nil.x affects every nil.

for a senior

Diagnose a cache or deep-copy failure that traces back to a test stub, and move long-lived stubs to tools that restore originals.

for a principal

Set test-suite rules against hand-written stubs on classes and shared objects, since the leaks they cause show up as order-dependent failures.

## Why a per-object method has side effects A singleton method is convenient in a test: build an object, give it a `def obj.charge`, pass it in. But the method lives in a **singleton class** attached to the object, and other parts of Ruby notice that class. ## Marshal refuses objects with singleton methods `Marshal.dump` serializes an object as its class name plus its data, and `Marshal.load` rebuilds it by looking the class up again. Code cannot be stored that way. So when the object's singleton class holds **methods or instance variables**, dumping raises `TypeError` with the message `singleton can't be dumped`. The failure surfaces far from the `def`: - a cache layer that stores values with `Marshal`; - a deep copy made with `Marshal.load(Marshal.dump(obj))`; - any code that sends objects to another process in Marshal format. An object whose singleton class only has a module mixed into it can still be dumped, because Marshal records the module by name. It is the methods defined directly on the object that cannot travel. ## Receivers that refuse outright | Receiver | Result of `def obj.x` or `obj.singleton_class` | |---|---| | Integer, Float, Symbol | `TypeError`: can't define singleton | | a frozen object | `FrozenError`, because the singleton class is frozen with it (an interned frozen string, such as a frozen literal, raises `TypeError` instead) | | `nil`, `true`, `false` | accepted, but defined on `NilClass`, `TrueClass`, `FalseClass` | | an ordinary object | its own singleton class | The `nil` row deserves attention in tests. `def nil.charge` does not stub one value: it adds `charge` to `NilClass`, so every `nil` in the test run responds to it, and specs that expect `NoMethodError` on `nil` start to pass for the wrong reason. ## Lifetime: the method lasts as long as the object A singleton method disappears only when its object is garbage-collected (or the method is removed explicitly). That is harmless for an object built inside one test and dropped afterwards, and dangerous for objects that live for the whole process: 1. **Classes and modules.** `def Clock.now` is a class method replacement; it stays for every later test. 2. **Constants and memoized singletons.** A shared client object stored in a constant keeps the stub. 3. **`nil`, `true`, `false`.** As above, they patch core classes. This is the practical reason test-double libraries exist: they record what they replaced and restore it after each test, which a hand-written `def` does not. ## Safer shapes for the same test - **Pass a stand-in object.** If the code under test receives its gateway as an argument, a tiny class with a `charge` method needs no patching and marshals normally. - **Stub through a test-double tool** when the object is shared, so the original comes back after the test. - **Keep per-object methods for throwaway objects** built inside one test, where lifetime and Marshal rarely matter. Each of these keeps the change visible in the test itself and bounded to it, which is what a hand-written singleton method on a long-lived object cannot promise. ## A checklist before using def obj.x in a test - Is the object created by this test and not shared? If not, use a tool that restores the original. - Will the object pass through anything that marshals it? If so, pass a small stand-in object instead. - Is the receiver an Integer, Symbol, Float, frozen object or `nil`? Then the trick fails or leaks. - Does the body need test locals? Then `define_singleton_method` with a block is required. ## Mistakes to avoid - Stubbing a method on a class with `def Klass.x` and forgetting that it outlives the test. - Expecting a `Marshal` round trip to preserve per-object methods. - Using `def nil.x` or `def true.x` as if they affected one value. - Trying to give a Symbol or Integer a per-value method.

  • Why can Marshal dump an object that was only extended with a named module, but not one with a def obj.x method?
    Marshal records modules mixed into an object's singleton class by name and re-applies them on load, since the module can be found again by that name. A method written directly into the singleton class has no name to look up, so Marshal raises `TypeError` rather than lose it silently.
  • A test does def PaymentClient.timeout = 0. Why do later tests start failing?
    `PaymentClient` is a class object that lives for the whole process, so the singleton method replaces its class method for every later test. Nothing restores the original. Use a test-double tool that resets after each test, or inject the dependency so the test can pass its own object.

saying these in an interview costs you the question

  • Marshal.dump copies an object's singleton methods along with its data
  • def nil.charge stubs a single nil value
  • Symbols accept singleton methods like any other object
  • Freezing an object still lets you add singleton methods
  • A singleton method on a class is removed when the test finishes