5

I want to define a constant FOO in the namespace of Integer that is similar to Float::NAN in Float, which is itself an instance of Float. It will be used somewhat similar to symbols, i.e., to mark a special case (of an integer). I don't need it to be used in calculation, but I need it to have the following properties:

  • Its class must be Integer or a subclass of Integer, and it must behave so to methods related to class:

    Integer::FOO.kind_of?(Integer) # => true
    

    Optionally (if the class is Integer):

    Integer::FOO.class # => Integer
    Integer === Integer::FOO # => true
    Integer::FOO.instance_of?(Integer) # => true
    
  • It must be distinct from (ideally all) other integers:

    Integer::FOO == 0 # => false
    

    Ideally, I want it distinct from any other integer, but if that is not possible, I can live with a dirty hack that, say makes Integer::FOO be identical to the largest or the smallest integer, which are the least likely to hit any random given integer.

What is the best way to go about this?

Max
  • 21,123
  • 5
  • 49
  • 71
sawa
  • 165,429
  • 45
  • 277
  • 381
  • NAN results in floating point because the binary representation does not translate into a valid number. That does not happen with binary integers. It seems you know that. Can you not define a constant NAN that is (64 bit) 0xffffffff. This is (signed int) -1, as unsigned int, it is the largest 64 bit integer. Doing this will confuse anybody who understands floating point however. – jim mcnamara May 28 '14 at 12:25
  • 2
    Regarding "What is the best way to go about this?", the best way is to not do this. Ruby really doesn't want you to muck around with base classes like that. I consider all of the answers (including my own) to be ugly hacks of academic interest at best. Whatever your use case is, there must be a different approach that avoids this mess. – Max May 28 '14 at 16:07

3 Answers3

3

Ruby's metaprogramming methods make it easy to twist a generic object into the shape you desire:

class Integer
  FOO = Object.new
end

Integer::FOO.define_singleton_method(:kind_of?) do |klass|
  Integer.ancestors.include? klass
end

Integer::FOO.define_singleton_method(:class) do
  Integer
end

Integer::FOO.define_singleton_method(:instance_of?) do |klass|
  klass == Integer
end

oldteq = Integer.method(:===)

Integer.define_singleton_method(:===) do |obj|
  obj == Integer::FOO ? true : oldteq.call(obj)
end

Integer::FOO.kind_of? Integer
# true
Integer::FOO.class
# Integer
Integer === Integer::FOO
# true
Integer::FOO.instance_of? Integer
# true
Integer::FOO == 0
# false

The tricky part is making sure you cover all of the use cases. My code handles all of the requirements you listed but I have no idea what kind of weird side effects such a strange object would create.

Max
  • 21,123
  • 5
  • 49
  • 71
  • Do I need to define them all? I expected some dependency between the methods. I am not sure which one depends on which, but for example, if the result of applying the method `class` is altered, then `===`, and perhaps some others might be dependent on it. – sawa May 28 '14 at 15:15
  • 2
    I was hoping that would be the case as well, but a lot of these methods invoke C code directly. For example `===` calls the C function `rb_obj_is_kind_of`, ignoring `kind_of?` in the singleton. – Max May 28 '14 at 15:25
3

Another option is to create a true Integer instance using a C extension:

// IntegerFoo.c
#include "ruby.h"

void Init_integer_foo() {
  // this should be the equivalent of "Integer.new"
  NEWOBJ_OF(obj, struct RObject, rb_cInteger, T_OBJECT | (RGENGC_WB_PROTECTED_OBJECT ? FL_WB_PROTECTED : 0));
  rb_define_const(rb_cInteger, "FOO", (VALUE)obj);
}
# extconf.rb
require 'mkmf'
dir_config('integer_foo')
create_makefile('integer_foo')

After building the extension ...

$ ruby extconf.rb
creating Makefile
$ make
compiling IntegerFoo.c
linking shared-object integer_foo.bundle

... the new constant can be used in Ruby and it seems to work as expected:

require './integer_foo'

Integer::FOO                      #=> #<Integer:0x007fe40c02c040>

Integer::FOO.kind_of? Integer     #=> true
Integer::FOO.class                #=> Integer
Integer === Integer::FOO          #=> true
Integer::FOO.instance_of? Integer #=> true
Integer::FOO == 0                 #=> false
Stefan
  • 109,145
  • 14
  • 143
  • 218
  • Nice! I'm surprised that this is possible in C but not in Ruby. It seems like they went through a lot of effort to make it hard to do. – Max May 28 '14 at 15:49
  • 1
    Indeed, Ruby explicitly "undefines" `allocate` and `new` – Stefan May 28 '14 at 15:53
0

I think the hack of picking an number you won't encounter anywhere is likely to be the cleanest solution.

class Integer
  FOO = (1..100).map{rand(10)}.join.to_i
end

Satisfies all of your criteria, aside from being equal to a random 100-digit number.

You can still introduce some custom behavior with this trick:

# Allow singleton methods on Bignums
class Bignum
  def singleton_method_added(meth)
    true
  end
end

class Integer
  FOO = (1..100).map{rand(10)}.join.to_i
  FOO.define_singleton_method(:to_s){'FOO'}
end

#Disallow singleton methods on Bignums once we've added all we're going to
class Bignum
  def singleton_method_added(meth)
    super
  end
end

p Integer::FOO # FOO

1111111111111111111111111111111.define_singleton_method(:will_raise){}
histocrat
  • 2,291
  • 12
  • 21