Rubyライブラリの書き方を調べる(what_methods.rbを読む)

what_methodsとは

what_methodsはメソッドの名前を忘れたとき調べるのに便利なライブラリ。

gem i what_methods

でインストールし、irbを立ち上げて次のように打ってみよう。

irb(main):001:0> require "what_methods"
=> true
irb(main):002:0> [1,2,3].what? 3
[1, 2, 3].last == 3
[1, 2, 3].pop == 3
[1, 2, 3].length == 3
[1, 2, 3].size == 3
[1, 2, 3].count == 3
[1, 2, 3].sample == 3
[1, 2, 3].max == 3
=> [:last, :pop, :length, :size, :count, :sample, :max]

sampleはいつも同じ結果が出てくるわけじゃないからなんだかなあ(汗)と言いたくなるけど、「××を調べる/変換するメソッドってなんだっけ?」という時に役立つライブラリであることが分かる。

目的

Rubyが好きなのでライブラリの一つでも書いてみたいな、と思っているのだけど、作り方もわからないしネタも思い浮かばないので、公開されているライブラリの中から興味を持ったものを読んで勉強していく。あとオブジェクトが持ってるメソッドを総当りしているのは分かるけどwhat_methodsの作り方すぐに思いつけなかったので。

what_methods.rbを読む

what_methodsはwhat_methods.rbというひとつのファイルから成り立っている。
コメントを除けば50行ほどの短いコードである。
これを頭から一行ずつ愚直に読み解く。
処理のおおまかな流れは、Objectクラスをオープンしてwhat?を追加し、その中でWhatMethods::MethodFinderクラスに自分(self)と受け取った引数が等価になるメソッドを検索させ(find)表示している(show)。
Objectクラスをオープンしているコードは次のようになっている。

class Object
  def what?(*a)
    WhatMethods::MethodFinder.show(self, *a)
  end
  #...省略...
end

what?は可変長引数*aを受け取ってそれをselfとともにWhatsMethods::MethodsFinder.showに渡しているだけ。
ではshowは?

module WhatMethods
  class MethodFinder
    #...省略...
    def self.show( anObject, expectedResult, *args, &block)
      find( anObject, expectedResult, *args, &block).each { |name|
        print "#{anObject.inspect}.#{name}" 
        print "(" + args.map { |o| o.inspect }.join(", ") + ")" unless args.empty?
        puts " == #{expectedResult.inspect}" 
      }
    end
  end
end

findに受け取った引数を渡して見つかったmethodsを表示しているだけのようだ。
表示のロジックの中にfindがあっていいのか…?
ともあれwhat_methodsの主要な処理はfindの中にありそう。

module WhatMethods
  class MethodFinder
    #...省略...
    def self.find( anObject, expectedResult, *args, &block )
      stdout, stderr = $stdout, $stderr
      $stdout = $stderr = DummyOut.new
      # change this back to == if you become worried about speed and warnings.
      res = anObject.methods.
            select { |name| anObject.method(name).arity <= args.size }.
            select { |name| not @@blacklist.include? name }.
            select { |name| begin 
                     anObject.clone.method( name ).call( *args, &block ) == expectedResult; 
                     rescue Object; end }
      $stdout, $stderr = stdout, stderr
      res
    end
    #...省略...
  end
end

一行目、$stdoutと$stderrをローカル変数に退避。
二行目、標準出力を利用するメソッドを呼び出した場合、余計な情報を画面に表示しないように、DummyOutという何もしないwriteメソッドをもつクラスを用意して標準出力を潰している。

class DummyOut
  def write(*args)
  end
end

続く6行がもっともコアな処理。

      res = anObject.methods.
            select { |name| anObject.method(name).arity <= args.size }.
            select { |name| not @@blacklist.include? name }.
            select { |name| begin 
                     anObject.clone.method( name ).call( *args, &block ) == expectedResult; 
                     rescue Object; end }

最初のselectでは引数の数を条件にnameを弾いている。
2番目のselectでは次の@@blacklistに載っているmethodを弾く。実行すると危なかったり処理がそれに持って行かれちゃう処理かな?

@@blacklist = %w(daemonize display exec exit! fork sleep system syscall what? ed emacs mate nano vi vim)

最後のselectで、残ったanObjectの持っているメソッドをすべて実行しexpectedResultと比較する。
cloneする理由が一瞬わからなかったが、これは破壊的なメソッドに備えているのだと気づいた(というかそれしかclone使う理由ってないかも)。
ちなみにcloneはimmutableなオブジェクトでは使えないため、Objectクラスで次のように拡張されている。

  alias_method :__clone__, :clone
  def clone
    __clone__
  rescue TypeError
    self
  end

最後に$stdout, $stderrを元に戻し、resを返してfindの処理は終わり。showに戻り以下略。

というわけで

いくつかのテクは新しく知ったけど、あまりモジュールの勉強にはならなかった気がする(ぉぃ
何か読むのにオススメのコードがあったら教えて下さい。