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

  • Recipe 2.6, "Converting Between Numeric Bases"
  • Recipe 2.14, "Doing Math with Roman Numbers"
  • Recipe 8.8, "Delegating Method Calls to Another Object"
  • Recipe 10.8, "Responding to Calls to Undefined Methods"


Strings

Numbers

Date and Time

Arrays

Hashes

Files and Directories

Code Blocks and Iteration

Objects and Classes8

Modules and Namespaces

Reflection and Metaprogramming

XML and HTML

Graphics and Other File Formats

Databases and Persistence

Internet Services

Web Development Ruby on Rails

Web Services and Distributed Programming

Testing, Debugging, Optimizing, and Documenting

Packaging and Distributing Software

Automating Tasks with Rake

Multitasking and Multithreading

User Interface

Extending Ruby with Other Languages

System Administration



Ruby Cookbook
Ruby Cookbook (Cookbooks (OReilly))
ISBN: 0596523696
EAN: 2147483647
Year: N/A
Pages: 399

Flylib.com © 2008-2020.
If you may any questions please contact us: flylib@qtcs.net