succさえあればオッケー

どんなオブジェクトでも、<=>とsuccが定義されていれば、範囲オブジェクトが作れる。ということの例として、range.cには、

class Xs                # represent a string of 'x's
  include Comparable
  attr :length
  def initialize(n)
    @length = n
  end
  def succ
    Xs.new(@length + 1)
  end
  def <=>(other)
    @length <=> other.length
  end
  def inspect
    'x' * @length
  end
end

# r = Xs.new(3)..Xs.new(6)   #=> xxx..xxxxxx
# r.to_a                     #=> [xxx, xxxx, xxxxx, xxxxxx]
# r.member?(Xs.new(5))   

というのが出ている。「x, xx, xxx, ……」というn個のxを並べた集合に対して、xの個数(length)を比較基準とし、n個のxが並んだオブジェクトの次はn+1個のx、という風に定義することで、ちょっと不思議な範囲オブジェクトを生成したりしている。

で、何かぼくも作ってみようと思って、真っ先に素数列を思い浮かべたのだけど、素数判定アルゴリズムを検索して、実はそれが21世紀に入った今も非常にホットなトピックで最速アルゴリズムがやたら複雑そうであることとか、1、11、111、……と1を並べた数字に関しては、その桁数が素数のときのみ元の数字も素数である、というビックリの事実を発見したり、それを使ったRubyの驚くべき素数判定メソッド:

class Squaresclass Fixnum
  def prime?
    ('1' * self) !~ /^1?$|^(11+?)\1+$/
  end
end

13.prime?
=> true

とかに驚いたり、ともかく寄り道をしすぎているうちに、素数の話はお腹がいっぱいになってしまった。上の素数判定メソッドはFixnumクラスに対して定義していて、ははあ、これがオープンクラスの破壊力もとい便利さってやつかと、ちょっとそんなことも思った。

111111……という数字はレピュニット(repunit:repeating unitの造語)と呼ばれていて、より大きな素数のrepunitを探すというのはレクリエーション数学家にとって結構広く知られた問題のようだ。レピュニットを10進数以外に一般化して、基数を2としたものがメルセンヌ数なんだとか。もうちょっと驚いたのは、クルマの走行距離メーターに並んだ1の数字列を見て、そうとは知らずにレピュニットの問題にぶつかってブログにそのことをしたためていたのは、あのXMLの父、Tim Brayさんだったこと。世界は狭い。

ともあれ、何か自分でも範囲オブジェクトにできるクラスを作ってみたくて、無理に平方数のクラスを作ってみた。ほとんどサンプルのコードと同じだけど、まあ、やってみると少ーずつ勉強になるもんだと思った。inspectやto_sを定義する理由はp、putsあたりでちゃんとオブジェクトのID(?)じゃなくて、人間が読める内容が表示できるようにするためらしいとか、attrは引数が1つの場合はattr_readerと同義のメソッドだとか、本題と無関係なところでもいくつか学ぶことがあった(追記:attrは引数の付け方とかがattr_readerとかとだいぶ違うので、いっそなかったことにして忘れたほうが良さそうだ。下のコードでも、attrに続いて2つシンボルを並べてるけど、実際には後者は無視されてるっぽい)

あ、もう1つ、succメソッドを書き間違えてエラーが出たから気がついたけど、範囲オブジェクトを生成したからといって、すぐにsuccを使った配列のようなものが展開されているわけじゃなくて、to_aとかしたときに範囲の間の要素が計算されてるっぽいこと。そうじゃないかとは思ってたけど、やっぱりそうだったのね。

Class Squares
  include Comparable
  attr :square, :root
  def initialize(n)
    @root = n
    @square = n * n
  end
  def succ
    Squares.new(@root + 1)
  end
  def <=>(other)
    @square <=> other.square
  end
  def to_s
    sprintf "%d",@square
  end
  def inspect
    sprintf "%d",@square
  end
end
irb(main):002:0> a = Squares.new(5)..Squares.new(13)
=> 25..169
irb(main):003:0> a.to_a
=> [25, 36, 49, 64, 81, 100, 121, 144, 169]
irb(main):004:0> a.member?(Squares.new(15))
=> false
irb(main):011:0> a.member?(Squares.new(7))
=> true

しかし、

irb(main):013:0> (5..13).map{|i| i*i}
=> [25, 36, 49, 64, 81, 100, 121, 144, 169]

とやればいいだけだし、やっぱりこの例には意味がないよな……。