Why Can't Singleton Methods Be Defined on Symbols or Fixnums

Why can't singleton methods be defined on Symbols or Fixnums?

This has to do with a concept called 'immediate values' as described here by Matz.

In truth, no immediate values should permit a singleton method. However, in the case of true, false, and nil, there are actually singleton classes that back these values (or the value is actually the singleton class - I'm not sure about this). You can therefore add singleton instances to the backing class which manifests as though it were the value itself. Numeric and Symbol instances are not singletons (obviously) and have nowhere to hold singleton methods.

Why can't singleton methods be defined on Symbols or Fixnums?

This has to do with a concept called 'immediate values' as described here by Matz.

In truth, no immediate values should permit a singleton method. However, in the case of true, false, and nil, there are actually singleton classes that back these values (or the value is actually the singleton class - I'm not sure about this). You can therefore add singleton instances to the backing class which manifests as though it were the value itself. Numeric and Symbol instances are not singletons (obviously) and have nowhere to hold singleton methods.

Defining methods on instances of Fixnum's

There are two things at play here.

One thing is that Fixnums can't have singleton methods. But, we aren't even at that point yet, since your code has a syntax error, and thus Ruby doesn't even attempt to run it in the first place.

The second thing is that Ruby's syntax is complex, and thus there are many dark corner cases. You seem to have found one, where the differing uses of the . symbol to mean both a decimal separator and a method selector conflict with each other in mysterious ways.

Now, of course, this isn't actually much of a problem, since, as I mentioned earlier, Fixnums can't have singleton class anyway:

object = 3

def object.foo
puts "3"
end
# TypeError: can't define singleton

mutator methods with Fixnum and Symbol

[...] immediate values cannot have singleton methods defined on them

Singleton method is one you define on an object of instance of class, not on class (object) itself, like in the example you've provided.

Consider following code:

s = :sym

def s.my_method
puts "HELLO"
end
# => TypeError: can't define singleton

When on other object, eg of string:

str = "string"

def str.my_method
puts "HELLO"
end

str.my_method
# => HELLO

The second one is without any errors.

Hope that helps!

UPDATE - what are immediate values

Fixnum are objects that contain immediate values

What does it means? It means that if we write x = 5, the number 5 will
be stored (encoded) directly into the variable x like if it was a
primitive data type (with ruby traditional objects, x would have
contained an address corresponding to the object stored in the heap).
So, when we write x = 5, x knows everything it needs about the number
5. In fact, x IS the object and not a reference to it.

reference

Let's consider an example:

fix1 = 2
fix2 = 2

fix1.object_id
# => 5

fix2.object_id
# => 5

But:

str1 = "test"
str2 = "test"

str1.object_id
# => 2157416420

str2.object_id
# => 2157531060

Fixnums of the same value have exactly the same object_id (the same applies to Symbol), but to Strings, even if they are of the similar value "test", they have different object_ids.

Because immediate values are immutable (are allocating the same place in memory), they can't be mutable.

Some `Fixnum` properties

In Ruby, most objects require memory to store their class and instance variables. Once this memory is allocated, Ruby represents each object by this memory location. When the object is assigned to a variable or passed to a function, it is the location of this memory that is passed, not the data at this memory. Singleton methods make use of this. When you define a singleton method, Ruby silently replaces the objects class with a new singleton class. Because each object stores its class, Ruby can easily replace an object's class with a new class that implements the singleton methods (and inherits from the original class).

This is no longer true for objects that are immediate values: true, false, nil, all symbols, and integers that are small enough to fit within a Fixnum. Ruby does not allocate memory for instances of these objects, it does not internally represent the objects as a location in memory. Instead, it infers the instance of the object based on its internal representation. What this means is twofold:

  1. The class of each object is no longer stored in memory at a particular location, and is instead implicitly determined by the type of immediate object. This is why Fixnums cannot have singleton methods.

  2. Immediate objects with the same state (e.g., two Fixnums of integer 2378) are actually the same instance. This is because the instance is determined by this state.

To get a better sense of this, consider the following operations on a Fixnum:

>> x = 3 + 7
=> 10
>> x.object_id == 10.object_id
=> true
>> x.object_id == (15-5).object_id
=> true

Now, consider them using strings:

>> x = "a" + "b"
=> "ab"
>> x.object_id == "ab".object_id
=> false
>> x.object_id == "Xab"[1...3].object_id
=> false
>> x == "ab"
=> true
>> x == "Xab"[1...3]
=> true

The reason the object ids of the Fixnums are equal is that they're immediate objects with the same internal representation. The strings, on the other hand, exist in allocated memory. The object id of each string is the location of its object state in memory.

Some low-level information

To understand this, you have to understand how Ruby (at least 1.8 and 1.9) treat Fixnums internally. In Ruby, all objects are represented in C code by variables of type VALUE. Ruby imposes the following requirements for VALUE:

  1. The type VALUE is is the smallest integer of sufficient size to hold a pointer. This means, in C, that sizeof(VALUE) == sizeof(void*).

  2. Any non-immediate object must be aligned on a 4-byte boundary. This means that any object allocated by Ruby will have address 4*i for some integer i. This also means that all pointers have zero values in their two least significant bits.

The first requirement allows Ruby to store both pointers to objects and immediate values in a variable of type VALUE. The second requirement allows Ruby to detect Fixnum and Symbol objects based on the two least significant bits.

To make this more concrete, consider the internal binary representation of a Ruby object z, which we'll call Rz in a 32-bit architecture:

MSB                                   LSB
3 2 1
1098 7654 3210 9876 5432 1098 7654 32 10
XXXX XXXX XXXX XXXX XXXX XXXX XXXX AB CD

Ruby then interprets Rz, the representation of z, as follows:

  1. If D==1, then z is a Fixnum. The integer value of this Fixnum is stored in the upper 31 bits of the representation, and is recovered by performing an arithmetic right shift to recover the signed integer stored in these bits.

  2. Three special representations are tested (all with D==0)

    • if Rz==0, then z is false
    • if Rz==2, then z is true
    • if Rz==4, then z is nil
  3. If ABCD == 1110, then 'z' is a Symbol. The symbol is converted into a unique ID by right-shifting the eight least-significant bits (i.e., z>>8 in C). On 32-bit architectures, this allows 2^24 different IDs (over 10 million). On 64-bit architectures, this allows 2^48 different IDs.

  4. Otherwise, Rz represents an address in memory for an instance of a Ruby object, and the type of z is determined by the class information at that location.



Related Topics



Leave a reply



Submit