Extending Immediate Types and Source Locations in Ruby


Overridding singleton classes of instances

In Ruby, most types allow you to open up their singleton classes to override instance methods:

class SomeType
  def value
    "some_type"
  end
end

a = SomeType.new

puts a.value
# => some_type


class << a
  def value
    "singleton_class"
  end
end

puts a.value
# => singleton_class

s = "symbol"

class << s
  def to_sym
    :lobmys
  end
end
puts s.to_sym.inspect
# => :lobmys

Immediate values

Some objects are implemented as immediate values in Ruby, and cannot be implemented have singleton methods defined on them directly:

Fixnumtruenil, and false are implemented as immediate values. With immediate values, variables hold the objects themselves, rather than references to them.

Singleton methods cannot be defined for such objects. Two Fixnums of the same value always represent the same object instance, so (for example) instance variables for the Fixnum with the value 1 are shared between all the 1’s in the system. This makes it impossible to define a singleton method for just one of these.

https://www.ruby-lang.org/en/documentation/faq/6/

DelegateClass wrapper

If you have a use case for extending the behavior of an immediate type, you can create a DelegateClass-wrapped subclass of that type and define the method on the subclass:

require "delegate"
 
class IntegerProxy < DelegateClass(Integer)
  def |(other)
    print "#{self.to_s(2)} | #{other.to_s(2)} => "
    IntegerProxy.new(super).tap { |new_value| puts new_value.to_s(2) }
  end
end

The above code lets you dump every | operation and its outcome. One thing to note is the IntegerProxy.new(super) which creates an instance of the wrapped immediate value for the new result. One downside of wrapping immediate values is that you no longer share the same space for two different instances of the same value, but this may make for more efficient debugging than a full trace function.

select_thing=IntegerProxy.new(0)
select_thing |= 0b1001
# => 0 | 1001 => 1001
select_thing |= 0b10010
# => 1001 | 10010 => 11011

Trying to see where a method is defined

Another useful thing if you have overriding of methods that may overlap is being able to tell where the version that you’re calling was defined. You can do this by substituting method(:method_name).source_location for method_name wherever you would normally invoke the method:

not_proxied=0

puts "select_thing.method(:|).source_location # => #{select_thing.method(:|).source_location}"
# => select_thing.method(:|).source_location # => ["integer_proxy_test.rb", 53]
puts "not_proxied.method(:|).source_location #=> #{0.method(:|).source_location.inspect}"
# => not_proxied.method(:|).source_location #=> nil

Note that the immediate type’s method (and if I recall correctly, any implementation that’s in C code) returns nil. However, the DelegateClass method returns the source file and line number of its definition.

Conclusion

Both DelegateClass and .method(symbol).source_location are probably not going to go into your production code, but as the source code and the problems get more complex to filter through, you may find usefulness in reaching for them to prove or disprove your debugging theories.


Leave a Reply