Simulating a Subclass of Fixnum

Problem

You want to create a class that acts like a subclass of Fixnum, Float, or one of Ruby's other built-in numeric classes. This wondrous class can be used in arithmetic along with real Integer or Float objects, and it will usually act like one of those objects, but it will have a different representation or implement extra functionality.

Solution

Let's take a concrete example and consider the possibilities. Suppose you wanted to create a class that acts just like Integer, except its string representation is a hexadecimal string beginning with "0x". Where a Fixnum's string representation might be "208", this class would represent 208 as "0xc8".

You could modify Integer#to_s to output a hexadecimal string. This would probably drive you insane because it would change the behavior for all Integer objects. From that point on, nearly all the numbers you use would have hexadecimal string representations. You probably want hexadecimal string representations only for a few of your numbers.

This is a job for a subclass, but you can't usefully subclass Fixnum (the Discussion explains why this is so). The only alternative is delegation. You need to create a class that contains an instance of Fixnum, and almost always delegates method calls to that instance. The only method calls it doesn't delegate should be the ones that it wants to override.

The simplest way to do this is to create a custom delegator class with the delegate library. A class created with DelegateClass accepts another object in its constructor, and delegates all methods to the corresponding methods of that object.

require 'delegate' class HexNumber < DelegateClass( Fixnum) # The string representations of this class are hexadecimal numbers def to_s sign = self < 0 ? "-" : "" hex = abs.to_s(16) "#{sign}0x#{hex}" end def inspect to_s end end HexNumber.new(10) # => 0xa HexNumber.new(-10) # => -0xa HexNumber.new(1000000) # => 0xf4240 HexNumber.new(1024 ** 10) # => 0x10000000000000000000000000 HexNumber.new(10).succ # => 11 HexNumber.new(10) * 2 # => 20

 

Discussion

Some object-oriented languages won't let you subclass the "basic" data types like integers. Other languages implement those data types as classes, so you can subclass them, no questions asked. Ruby implements numbers as classes (Integer, with its concrete subclasses Fixnum and Bignum), and you can subclass those classes. If you try, though, you'll quickly discover that your subclasses are useless: they don't have constructors.

Ruby jealously guards the creation of new Integer objects. This way it ensures that, for instance, there can be only one Fixnum instance for a given number:

100.object_id # => 201 (10 * 10).object_id # => 201 Fixnum.new(100) # NoMethodError: undefined method `new' for Fixnum:Class

You can have more than one Bignum object for a given number, but you can only create them by exceeding the bounds of Fixnum. There's no Bignum constructor, either. The same is true for Float.

(10 ** 20).object_id # => -606073730 ((10 ** 19) * 10).object_id # => -606079360 Bignum.new(10 ** 20) # NoMethodError: undefined method `new' for Bignum:Class

If you subclass Integer or one of its subclasses, you won't be able to create any instances of your classnot because those classes aren't "real" classes, but because they don't really have constructors. You might as well not bother.

So how can you create a custom number-like class without redefining all the methods of Fixnum? You can't, really. The good news is that in Ruby, there's nothing painful about redefining all the methods of Fixnum. The delegate library takes care of it for you. You can use this library to generate a class that responds to all the same method calls as Fixnum. It does this by delegating all those method calls to a Fixnum object it holds as a member. You can then override those classes at your leisure, customizing behavior.

Since most methods are delegated to the member Fixnum, you can perform math on HexNumber objects, use succ and upto, create ranges, and do almost anything else you can do with a Fixnum. Calling HexNumber#is_a?(Fixnum) will return false, but you can change even that by manually overriding is_a?.

Alas, the illusion is spoiled somewhat by the fact that when you perform math on HexNumber objects, you get Fixnum objects back.

HexNumber.new(10) * 2 # => 20 HexNumber.new(10) + HexNumber.new(200) # => 210

Is there a way to do math with HexNumber objects and get HexNumber objects as results? There is, but it requires moving a little bit beyond the comfort of the delegate library. Instead of simply delegating all our method calls to an Integer object, we want to delegate the method calls, then intercept and modify the return values. If a method call on the underlying Integer object returns an Integer or a collection of Integers, we want to convert it into a HexNumber object or a collection of HexNumbers.

The easiest way to delegate all methods is to create a class that's nearly empty and define a method_missing method. Here's a second HexNumber class that silently converts the results of mathematical operations (and any other Integer result from a method of Integer) into HexNumber objects. It uses the BasicObject class from the Facets More library (available as the facets-more gem): a class that defines almost no methods at all. This lets us delegate almost everything to Integer.

require 'rubygems' require 'facet/basicobject' class BetterHexNumber < BasicObject def initialize(integer) @value = integer end # Delegate all methods to the stored integer value. If the result is a # Integer, transform it into a BetterHexNumber object. If it's an # enumerable containing Integers, transform it into an enumerable # containing BetterHexNumber objects def method_missing(m, *args) super unless @value.respond_to?(m) hex_args = args.collect do |arg| arg.kind_of?(BetterHexNumber) ? arg.to_int : arg end result = @value.send(m, *hex_args) return result if m == :coerce case result when Integer BetterHexNumber.new(result) when Array result.collect do |element| element.kind_of?(Integer) ? BetterHexNumber.new(element) : element end else result end end # We don't actually define any of the Fixnum methods in this class, # but from the perspective of an outside object we do respond to # them. What outside objects don't know won't hurt them, so we'll # claim that we actually implement the same methods as our delegate # object. Unless this method is defined, features like ranges won't # work. def respond_to?(method_name) super or @value.respond_to? method_name end # Convert the number to a hex string, ignoring any other base # that might have been passed in. def to_s(*args) hex = @value.abs.to_s(16) sign = self < 0 ? "-" : "" "#{sign}0x#{hex}" end def inspect to_s end end

Now we can do arithmetic with BetterHexNumber objects, and get BetterHexNumber object back:

hundred = BetterHexNumber.new(100) # => 0x64 hundred + 5 # => 0x69 hundred + BetterHexNumber.new(5) # => 0x69 hundred.succ # => 0x65 hundred / 5 # => 0x14 hundred * -10 # => -0x3e8 hundred.divmod(3) # => [0x21, 0x1] (hundred…hundred+3).collect # => [0x64, 0x65, 0x66]

A BetterHexNumber even claims to be a Fixnum, and to respond to all the methods of Fixnum! The only way to know it's not is to call is_a?.

hundred.class # => Fixnum hundred.respond_to? :succ # => true hundred.is_a? Fixnum # => false

 

See Also

Категории