もんだいの解答

mosaic

もんだいの解答。

いくつかの解答が挙げられていますが、問題文には仕様も前提もろくに書かれていないので、インチキもまとももなくみんな正解です。僕が想定していた解答はこんな感じ。

def div(x, y)
  puts [111111111, true, 12345678987654321]
  exit
end

嘘です。


基本的な方針は i のメソッドの上書きです。つまり例えば、i.inspect の結果が i の実際の値と関係なく 111111111 を返せばいいわけです。Bignum#inspect ごと上書きしてしまうのも手ですが、他の Bignum にも影響が及ぶのであまり気分がよくないです。こういうときはやっぱり i の特異メソッドとして上書きしたいですよね。まずは p(i) #=> 111111111 だけ目指すとして、考えられるのは次のようなコードです。

def div(x, y)
  class << x
    def inspect
      111111111
    end
  end
end

i = 12345678987654321
div(i, 111111111)
p(i)

特異メソッド inspect に、実際の値に関係なく 111111111 を返させます。ですが、このコードは動きません。

$ ruby div.rb
div.rb:3:in `div': can't define singleton method "inspect" for Bignum (TypeError)
        from div.rb:10

ここで諦めてはいけません。Fixnum は unboxed な値として実装されているので、実装上の都合から特異メソッドを持たせられないのですが、Bignum は boxed なのでできるはずです。そこでこのエラーメッセージで ruby のソースを検索したところ、こうなっていました。

static VALUE
num_sadded(VALUE x, VALUE name)
{
    /* ruby_frame = ruby_frame->prev; */ /* pop frame for "singleton_method_added" */
    /* Numerics should be values; singleton_methods should not be added to them */
    rb_raise(rb_eTypeError,
             "can't define singleton method \"%s\" for %s",
             rb_id2name(rb_to_id(name)),
             rb_obj_classname(x));
    return Qnil;                /* not reached */
}
...(略)...
    rb_define_method(rb_cNumeric, "singleton_method_added", num_sadded, 1);

singleton_method_added とは Object クラスのメソッドで、インスタンスに特異メソッドが定義されたことの通知を受け取るメソッドです。つまりこの例外は singleton_method_added から投げられているということになります *1 。従って、まっとう (?) な解答としては、先に singleton_method_added を上書きしてしまえばいいです。

def div(x, y)
  class << x
    # 今後特異メソッドが定義されたことの通知はこれが受け取る
    def singleton_method_added(x); end
    # 特異メソッドを定義する (例外は投げられない)
    def inspect
      111111111
    end
    # 元に戻しておく
    remove_method :singleton_method_added
  end
end

i = 12345678987654321
div(i, 111111111)
p(i)

これで p(i) が動きます。

$ ruby div.rb
111111111

一般化して、想定していた解答は次のようになります。一般化のやり方は jijixi さんの MutableInteger と同じ発想です。

def div(x, y)
  # 計算結果を保持する
  x.instance_eval { @val = x / y }
  class << x
    def singleton_method_added(x); end
    # (ほぼ) すべてのインスタンスメソッドを再定義して @val にたらい回す
    instance_methods.each do |m|
      # singleton_method_added は再定義しない
      next if m == "singleton_method_added"
      # 再定義で warning が出るのは鬱陶しいので再定義しない
      next if m == "__send__" || m == "__id__" # for 1.8
      next if m == :object_id || m == :__send || m == :__send! # for 1.9
      # 再定義する
      eval <<-END
        def #{ m }(*args)
          @val.__send__(#{ m.to_sym.inspect }, *args)
        end
      END
    end
    remove_method :singleton_method_added
  end
end

i = 12345678987654321
div(i, 111111111)
p(i)               #=> 111111111
p(i == 111111111)  #=> true
p(i * 111111111)   #=> 12345678987654321


だがちょっと待って欲しい。問題のメソッド名は singleton_method_added です。過去形です (いや、過去分詞形) 。このメソッドは、特異メソッドが定義されたに呼ばれます。つまり、can't define singleton method の例外は、定義がされた後に投げられています。従って、次のようなコードでも、

def div(x, y)
  class << x
    # 特異メソッドを定義する (定義後に TypeError が投げられる)
    def inspect
      111111111
    end
  rescue TypeError # 拾う
  end
end

i = 12345678987654321
div(i, 111111111)
p(i)

実は動いてしまいます。これって想定された動作なの?

$ ruby div.rb
111111111

この例外に悩んだ人は、実はその時点でほぼ正解していたわけですね。Ruby で嫌いな人と共同開発をするときは、こういう機能をふんだんに使って陥れましょう (嘘)


僕の想定範囲外の解答として、id:mr_konn さんの TOPLEVEL_BINDING を使った解答 (とそれの改良版) は面白いです。また、yowa さんの p を再定義する解答は愚かにも気がついてませんでした。基本なのに。例えばこんな感じ?

def div(x, y)
  self.instance_eval do
    def p(x)
      super(x.is_a?(Integer) ? x / 111111111 : !x)
    end
  end
end

*1:あとで気がつきましたが、このことは ruby 1.8 feature に (こっそり) 書いてありました。