RubyのStructのイディオムとアンドキュメンテッドな機能

改めてStruct関連のイディオムについて調べた。Ruby標準のStructクラスを使えばコンストラクタでの面倒な初期化を飛ばせる。以下のクラスBとクラスCは、ほぼ同じ機能を実現する。

class B
  attr_accessor :var1, :var2

  def initialize(var1, var2)
    @var1, @var2 = var1, var2
  end
end

B.new("bar", 5)
=> #<B:0x007fae7a93ddb8 @var1="bar", @var2=5>
class C < Struct.new(:var1, :var2)
end

C.new("foo", 3)
=> #<struct C var1="foo", var2=3>

Structを使うとインスタンス変数が生成されるわけではないけど、アクセッサ経由で読み書きできるというインターフェイスは同じ。振る舞いが少なく、属性だけがいろいろとあるクラスをサッと定義したいようなときにStructは使える。テストコードのモックオブジェクトを作るようなときにも使われているようだ。

アクセッサが定義されているのは以下のように確認できる。

> Point = Struct.new :x, :y
=> Point
> p = Point.new 3, 4
=> #<struct Point x=3, y=4>
> Point.instance_methods false
=> [:x, :x=, :y, :y=]

で、Structには3つの使い方がある。基本的にStruct.newは無名クラス(Classクラスのインスタンス)を生成する。

(1) Struct.newを継承して新しいクラスを定義する

> class Point < Struct.new(:x, :y); end
> p = Point.new(1,2)
=> #<struct Point x=1, y=2>
> p.x
=> 1
> p.x = 3
=> 3
> p.x
=> 3

(2) Struct.newの返り値がClassクラスのインスタンスなのでそれを定数に入れる
> P = Struct.new(:x, :y)
> p = P.new 1, 2
=> #<struct P x=1, y=2>

(3) Struct.newの第1引数に文字列でクラス名を渡す
> Struct.new("Point", :x, :y)
=> Struct::Point
> p = Struct::Point.new 1, 2
=> #<struct Struct::Point x=1, y=2>

最初の2つはほぼ同義。ただ、2つ目のやり方だと、クラスに振る舞いを付け加えるときに再び明示的にクラスをオープンすることになるので、やや不自然。

Point = Struct.new(:x, :y)
class Point
  def distance
    Math::sqrt(x**2 + y**2)
  end
end

Point.class_eval do
  def minus
    x - y
  end
end

p = Point.new(3, 4)
p p
p p.distance
p p.minus

# #<struct Point x=3, y=4>
# 5.0
# -1

……と思ったけど、実はStruct.newはブロックを受け取って、それをmodule_evalするというアンドキュメンテッドな機能がある。これを使うと以下のように書ける。

Point = Struct.new(:x, :y) do
  def distance
    Math::sqrt(x**2 + y**2)
  end
end

p Point.new(3, 4).distance  # => 5.0

ruby/struct.cを見てみると、Struct.newは以下のようになっている。

static VALUE
rb_struct_s_def(int argc, VALUE *argv, VALUE klass)
{
    VALUE name, rest;
    long i;
    VALUE st;
    ID id;

    rb_scan_args(argc, argv, "1*", &name, &rest);
    if (!NIL_P(name) && SYMBOL_P(name)) {
	rb_ary_unshift(rest, name);
	name = Qnil;
    }
    for (i=0; i<RARRAY_LEN(rest); i++) {
	id = rb_to_id(RARRAY_PTR(rest)[i]);
	RARRAY_PTR(rest)[i] = ID2SYM(id);
    }
    st = make_struct(name, rest, klass);
    if (rb_block_given_p()) {
	rb_mod_module_eval(0, 0, st);
    }

    return st;
}

下から4行目ぐらい。ブロックがあれば、module_evalせよとある。このコードは2004年3月に入ったようだ。なぜ9年近く前のことなのに、いまだにドキュメントに書かれていないのだろうか? ちょっと理由が分からない。

Struct.newにブロックを渡す用法には落とし穴がある。ぱっと見ると、クラス定義っぽく見えるけど、class文のようにコンテキストが切り替わらない。

Point = Struct.new(:x, :y) do
  K = 42
  p self  => #<Class:0x007f90d386fcc0>
  def distance
    Math::sqrt(x**2 + y**2)
  end
end

p Point.new(3, 4).distance => 5.0
p Point::K # => st3.rb:9: warning: toplevel constant K referenced by Point::K

ということで、Structのイディオムとしては、

class Point < Struct.new(:x, :y); end

がバランスがいいように思う。Struct.newはsuper経由でも呼べるので、以下のようなこともできる。

class C < Struct.new(:var1, :var2)
  def initialize(var1, var2)
    s = var1.to_sym
    super(s, var2)
  end
end

C.new("foo", 3)
=> #<struct C var1=:foo, var2=3>

そもそもJavaScriptとかを見ていると、もうなんでもハッシュでいいんじゃないのという気もするわけだけど、ハッシュだと、

  • 属性定義が動的すぎる
  • 属性名のスペルミスに気付きづらい

ということ。逆に、実行時になるまで属性の数や種類が分からないときはハッシュのほうがいい。

参考にしたページ:

しつこく Hub を読む

しつこく「Hub」を眺めている。語学学習に精読と多読の2つのアプローチがあるように、コードリーディングにも精読というのがあるはずで、試しにそれをやってみている。モヤッする「分かってない感」を感じたら、基本的に分かるまで調べるのが精読。そのためには、ブログエントリとして分かったことを書き出すのは良い方法のような気がしたのでやってみている。ほとんど誰も読んでないブログだとはいえ、アウトプットの形にするのは手間だし、何だか気分的には常に挫けそうなんだけど、「なんとなく分かった気になる」という厄介な悪癖を変えるには十分に時間に見合うと思う。

で、やってみて分かったけど、RubyのこともRubyのイディオムのことも、まるで分かってないということが良く分かった。「コードを読む」ということが何かも分かってなかったなと思った。なんとなく眺めてちゃ駄目だな。まず精読。そのうち多読ができればいいなと思っている。

コードを読むのは思ったよりずっと楽しい。理解できないところは「面倒だな」と感じるのだけど、まあ調べれば大抵分かる。で、そうやってイディオムや、コードのまとめ方、ライブラリの使い方を調べるのってプログラミング学習の王道なんだろうなと思った。今まで何をやっていたのだと思うようになった。これと同じ事をRuby以外の言語でもやりたい。

ともあれ。

Hub::Contextは gitコマンドを実行して結果をキャッシュしたり設定を読み出すためのクラス。頭からつらつら読んでいって、最初から意味が分からないところがあった。以下。

    # Shells out to git to get output of its commands
    class GitReader
      attr_reader :executable

      def initialize(executable = nil, &read_proc)
        @executable = executable || 'git'
        # caches output when shelling out to git
        read_proc ||= lambda { |cache, cmd|
          result = %x{#{command_to_string(cmd)} 2>#{NULL}}.chomp
          cache[cmd] = $?.success? && !result.empty? ? result : nil
        }
        @cache = Hash.new(&read_proc)
      end

%xはプロセスをフォークして子プロセスとしてコマンドなどを実行する。バックティックで文字列を囲んでも同じ。似たようなものに Kernel#system もあるけど、こちらは戻り値がtrue/falseであってコマンドの出力は取れない。

GitReaderでは、%xを使ってエラー出力をNULLに捨てつつ結果をchompしてresultに入れている。それはいいけど、「$?.success?」ってなんだ?

Ruby(などのスクリプト言語)の特殊な記号ってググるのが難しい。やっとそれらしいページが出てきたと思ったら主に使われる記号類のリストとかで「$?」が出てなかったりする。こういうとき手元にちゃんとドキュメントを入れておくんだったとか、公式ドキュメントにブックマークしておくんだったと思ったりする。

で、今回気付いたけど、特殊記号を調べる簡単な方法は、Rubyに標準で添付されているenglish.rbを見ることかも。english.rbは以下のようにずらずらとエイリアスを付けるライブラリだけど、ドキュメント代わりにも使える。

alias $DEFAULT_INPUT           $<

# The process number of the program being executed. Read only.
alias $PID                     $$

# The process number of the program being executed. Read only.
alias $PROCESS_ID              $$

# The exit status of the last child process to terminate. Read
# only. Thread local.
alias $CHILD_STATUS            $?

# A +MatchData+ object that encapsulates the results of a successful
# pattern match. The variables <tt>$&</tt>, <tt>$`</tt>, <tt>$'</tt>,
# and <tt>$1</tt> to <tt>$9</tt> are all derived from
# <tt>$~</tt>. Assigning to <tt>$~</tt> changes the values of these
# derived variables.  This variable is local to the current
# scope. Thread local.
alias $LAST_MATCH_INFO         $~

# If set to any value apart from +nil+ or +false+, all pattern matches
# will be case insensitive, string comparisons will ignore case, and
# string hash values will be case insensitive. Deprecated
alias $IGNORECASE              $=

ということで、「$?」は子プロセスの終了コードを保持していることが分かった。$?は何者かというと、

%x{ls}
=> "Gemfile\nGemfile.lock\nHISTORY.md\nLICENSE\nREADME.md\nRakefile\nbin\netc\nfeatures\ngit-hooks\nhub.gemspec\nlib\nman\ntest\ntmp\n"
[3] pry(main)> $?.class
=> Process::Status

ということで、Process::Statusインスタンスだと分かる。POSIXではプロセスのステータスを16ビットの数値で保持していて、それをカプセル化しているインスタンスらしい。なるほど。シェルやプロセスとやり取りするクラスやライブラリってRubyには色々あるんだなということで、今回 Shelljoin というものがあるのも知った。

もう1つ分からないのは、

def initialize(executable = nil, &read_proc)
  @executable = executable || 'git'

と書くことと、

def initialize(executable = 'git', &read_proc)
  @executable = executable

と書くことの違い。executableは、gitコマンドの文字列が入る。デフォルトで「git」、もし環境変数の GIT が設定されていれば、それを優先する。以下のように。

    def git_reader
      @git_reader ||= GitReader.new ENV['GIT']
    end

些細な話かもしれないけど、もし動作に違いがないなら、executable が nil になることはないのだし、ここは引数にデフォルト値を書いたほうがストレートな気がする。同様に、read_procを外から渡せるようにしている理由もよく分からない。テストで stub out するためにこうしているような気がするけど、ここはこれ以上は追わない。

なるほど面白いと思ったのがハッシュのコンストラクタにlambdaを渡す用法。

Rubyでは Hash.new でデフォルト値を渡せる。存在しないkeyでアクセスしようとすると、このデフォルトの値が返ってくる。

h = Hash.new("Go Fish")
h["a"] = 100
h["b"] = 200
h["a"]           #=> 100
h["c"]           #=> "Go Fish"
# The following alters the single default object
h["c"].upcase!   #=> "GO FISH"
h["d"]           #=> "GO FISH"
h.keys           #=> ["a", "b"]

デフォルトオブジェクトが書き換え可能というのにちょっと驚いた。Rubyっぽい柔らかさだ。

で、Hash.new にはcallableオブジェクトも渡せる。すると、存在しないkeyでアクセスしようとすると、このブロックが起動される。このときブロック変数として、そのハッシュそのものとkeyが渡る。これを使うと以下のように何らかのコンピュテーションなりIO処理なりの実行結果をキャッシュするような機構が手軽に実現できる。

class C
  def initialize
    default_proc ||= lambda {|hash, key|
      puts "excecuted"
      hash[key] = key.length
    }
    @cache = Hash.new(&default_proc)
  end

  def read(key)
    @cache[key]
  end
end
irb(main):001:0> c = C.new
=> #<C:0x007ffae2227a88 @cache={}>
irb(main):002:0> c.read("hoge")
excecuted
=> 4
irb(main):003:0> c
=> #<C:0x007ffae2227a88 @cache={"hoge"=>4}>
irb(main):004:0> c.read("foo")
excecuted
=> 3
irb(main):005:0> c
=> #<C:0x007ffae2227a88 @cache={"hoge"=>4, "foo"=>3}>
irb(main):006:0> c.read("this is just a hash")
excecuted
=> 19
irb(main):007:0> c
=> #<C:0x007ffae2227a88 @cache={"hoge"=>4, "foo"=>3, "this is just a hash"=>19}>
irb(main):008:0> c.read("foo")
=> 3

これが、Hub::Context::GitReaderの @cache の挙動。

hubには、他にもいくつか Hash.new(&block)を使っているところがあって、設定関連の情報をYAMLでファイルに書きだしたり読みだしたりする Hub::GitHubAPI::FileStore には、@data の初期化が以下のようになっている。

    # Filesystem store suitable for Configuration
    class FileStore
      extend Forwardable
      def_delegator :@data, :[], :get
      def_delegator :@data, :[]=, :set

      def initialize filename
        @filename = filename
        @data = Hash.new {|d, host| d[host] = [] }
        load if File.exist? filename
      end

ブロック変数の名前の付け方が利用目的に沿う形になっているので最初分からなかったけど、ここで d にはhashそのものである@dataへの参照が渡される。hostのほうはアクセスしようとしたkeyのオブジェクトが渡る。FileStore#getと#setでアクセスする@dataのデータ表現はハッシュだけど、中身はホスト名を表すkeyに対して常に配列になっているということだ。

Hub::Context::GitReaderで次に面白いなと思ったのは、APIの晒し方。

実際にgitコマンドの出力を、コマンドを実行したりキャッシュしたりしつつ返すのはGitReader#readというメソッドだけど、これは、以下のように別名を付けられている。

module Hub
  module Context
    class GitReader
    :
    end

    module GitReaderMethods
      extend Forwardable

      def_delegator :git_reader, :read_config, :git_config
      def_delegator :git_reader, :read, :git_command

      def self.extended(base)
        base.extend Forwardable
        base.def_delegators :'self.class', :git_config, :git_command
      end
    end

    private

    def git_reader
      @git_reader ||= GitReader.new ENV['GIT']
    end

    include GitReaderMethods
    private :git_config, :git_command

git_configはgit_commandの特殊ケースなので、本質的には

  • GitReaderMethodsでGitReaderを読み書きするAPIを定義(宣言)している
  • そのAPIはGitReader#git_commandで、これは git_reader というオブジェクトにreadというメッセージを送ることと委譲が定義されている
  • git_readerはGitReaderのインスタンス
  • git_commandはprivate

GitReaderMethodsはコードの読み手に「ここに列挙しておきますよ」と伝える役割りかな。git_commandがprivateなのは、gitコマンド関連で情報や設定の読み出しの操作は全てGitReader内で完結していて、対応するメソッドが存在するはずだし、今後何か拡張するとしても、GitReader内にメソッドを足して、そこで直接git_commandを実行せよ、という設計ガイドラインみたいなもんだろうか。

あいや……、違うな。git_readerは、以下のようにLocalRepoの初期化時に投げ込まれている。GitReaderMethodsのようにモジュールを切り出すのは再利用のためと、テストの書きやすさのためもあるのかな。

    class LocalRepo < Struct.new(:git_reader, :dir)
      include GitReaderMethods

      :
      :
      :

    def local_repo(fatal = true)
        :
        :
          LocalRepo.new git_reader, current_dir
    end

git/GitHubを便利に使うHubを少し読む(3)

git/GitHubを便利に使うRuby製ツールのコマンドラインツール「Hub」のソースコードを少し読んだので、あれこれメモ。これこれの続き。

GitHubAPIをラップする lib/hub/github_api.rb は、クラス定義関連を抜き出すと以下。

module Hub
  class GitHubAPI
    module Exceptions
    module HttpMethods
      module ResponseMethods
    module OAuth
    include HttpMethods
    include OAuth
    class FileStore
    class Configuration

HttpMethodsとOAuthは、定義直後に include している。ということは、これらはソースコードの読み手に意味的な構造を伝えているだけで、名前空間を切り分けているわけでもなければ、Hub::GitHubAPI以外のクラスでincludeしようとか、そういう理由から module としているわけではないのか?

試しに、以下のようにフラットな構造にして、テストを通してみる。

-    module HttpMethods
+#    module HttpMethods
       # Decorator for Net::HTTPResponse
       module ResponseMethods
         def status() code.to_i end
@@ -223,9 +223,9 @@ module Hub
         end
         return http
       end
-    end
+#    end
 
-    module OAuth
+#    module OAuth
       def apply_authentication req, url
         if (req.path =~ /\/authorizations$/)
           super
@@ -253,10 +253,10 @@ module Hub
           res.data['token']
         end
       end
-    end
+#    end
 
-    include HttpMethods
-    include OAuth
+#    include HttpMethods
+#    include OAuth
 

あれ、テストが落ちる。なんだろうと思ったら、module OAuthの apply_authentication では、実は super で継承ツリーの上にいるメソッドを呼んでいるのだった。関係するところを抜き出すと、

    module OAuth
      def apply_authentication req, url
        if (req.path =~ /\/authorizations$/)
          super
        else
          user = url.user || config.username(url.host)


    module HttpMethods

      def apply_authentication req, url
        user = url.user || config.username(url.host)
        pass = config.password(url.host, user)
        req.basic_auth user, pass
      end

    include HttpMethods
    include OAuth

となっている。moduleを include するというのは、本当にメソッドをクラスに混ぜ込んでいるわけではない。includeした瞬間にRuby処理系は「Class.new」相当の無名クラスを生成して、このクラスを継承チェーンに挿入している。挿入するのは、includeするクラスと、そのクラスのスーパークラスの間。例えば、

module M
  def foo
    puts "I'm a module."
  end
end

class C
  include M

  def foo
    puts "I'm a class."
    super
  end
end

C.new.foo

としたとき、この super は、M#fooの呼び出しに相当するので、

$ ruby include.rb
I'm a class.
I'm a module.

となる。つまり、include順も重要ということで、

    include HttpMethods
    include OAuth

をひっくり返すと、これまたテストがコケる。HttpMethodsとOAuthとで重複しているメソッドは、apply_authenticationだけなので、この2つを継承順に注意して逆リファクタリングすると、テストは通る。OAuthのapply_authenticationのsuperの部分に、HttpMethodsのapply_authenticationを展開して、それをHttpMethodsのほうに混ぜ込んで、moduleの階層を消す。

diff --git a/lib/hub/github_api.rb b/lib/hub/github_api.rb
index 32eb3ad..0696d48 100644
--- a/lib/hub/github_api.rb
+++ b/lib/hub/github_api.rb
@@ -115,7 +115,7 @@ module Hub
     # - proxy_uri(with_ssl)
     # - username(host)
     # - password(host, user)
-    module HttpMethods
+#    module HttpMethods
       # Decorator for Net::HTTPResponse
       module ResponseMethods
         def status() code.to_i end
@@ -197,9 +197,17 @@ module Hub
       end
 
       def apply_authentication req, url
-        user = url.user || config.username(url.host)
-        pass = config.password(url.host, user)
-        req.basic_auth user, pass
+        if (req.path =~ /\/authorizations$/)
+          user = url.user || config.username(url.host)
+          pass = config.password(url.host, user)
+          req.basic_auth user, pass
+        else
+          user = url.user || config.username(url.host)
+          token = config.oauth_token(url.host, user) {
+            obtain_oauth_token url.host, user
+          }
+          req['Authorization'] = "token #{token}"
+        end
       end
 
       def create_connection url
@@ -223,21 +231,9 @@ module Hub
         end
         return http
       end
-    end
-
-    module OAuth
-      def apply_authentication req, url
-        if (req.path =~ /\/authorizations$/)
-          super
-        else
-          user = url.user || config.username(url.host)
-          token = config.oauth_token(url.host, user) {
-            obtain_oauth_token url.host, user
-          }
-          req['Authorization'] = "token #{token}"
-        end
-      end
+#    end
 
+#    module OAuth
       def obtain_oauth_token host, user
         # first try to fetch existing authorization
         res = get "https://#{user}@#{host}/authorizations"
@@ -253,10 +249,10 @@ module Hub
           res.data['token']
         end
       end
-    end
+#    end
 
-    include HttpMethods
-    include OAuth
+#    include HttpMethods
+#    include OAuth
 
     # Filesystem store suitable for Configuration
     class FileStore

つまり、OAuthはOAuthのときのための差分を定義するモジュールということ。2つしかメソッドがない。

で、HttpMethodsが何をしているかというと、内部DSLをGitHubAPIクラスに提供している。Hub::GitHubAPIの典型的なAPIリクエストのメソッドは、以下のような感じになる。

    # Public: Fork the specified repo.
    def fork_repo project
      res = post "https://%s/repos/%s/%s/forks" %
        [api_host(project.host), project.owner, project.name]
      res.error! unless res.success?
    end

GitHubへのAPIコールは、すべてのこの形式で抽象化されている。文字列に変数を評価して埋め込むとき、#{} を使ったインターポレーションをすることがRubyでは多いけど、複数のデータを渡すなら、このように、String#% で流しこむのも見やすくていいかもなと思った。

ポイントは、

  • HTTP POST/GET は、post/getというメソッド名で抽象化されている
  • projectというオブジェクトは実際には色々あって、ダックタイピングの典型ぽい

というところかなと思う。

で、例えば、getのほうは、

def get url, &block
  perform_request url, :Get, &block
end

となっていて、さらにperform_requestを呼んでいる。パラメータで渡されている :Get は、

def perform_request url, type
  url = URI.parse url unless url.respond_to? :host

   require 'net/https'
   req = Net::HTTP.const_get(type).new request_uri(url)

のように、Net::HTTP::Getのクラスへの参照を持つ定数を引っ張りだすときに、const_getのリフレクションを利用する場面で使われている。

github_api.rb を見ていて改めて思ったけど、どんどん細かくメソッドを分けることで挙動に名前をつけるというのが、Rubyっぽい。例えば、コマンドラインから与えられたプロジェクトが存在するかどうかを確かめる GitHubAPI#repo_exists? というメソッドは以下のように、1行になっている。

    # Public: Fetch data for a specific repo.
    def repo_info project
      get "https://%s/repos/%s/%s" %
        [api_host(project.host), project.owner, project.name]
    end

    # Public: Determine whether a specific repo exists.
    def repo_exists? project
      repo_info(project).success?
    end

repo_infoを呼び出して、それが success? かどうかを確かめている。この success? というのは、net/httpのNet::HTTPResponseに混ぜ込むために用意してある、Hub::GitHubAPI::HTTPMethods::ResponseMethodsに以下のように定義されている。

    module HttpMethods
      # Decorator for Net::HTTPResponse
      module ResponseMethods
        def status() code.to_i end
        def data?() content_type =~ /\bjson\b/ end
        def data() @data ||= JSON.parse(body) end
        def error_message?() data? and data['errors'] || data['message'] end
        def error_message() error_sentences || data['message'] end
        def success?() Net::HTTPSuccess === self end

実際にこのモジュールは、perform_requestで、

        res = http.start { http.request(req) }
        res.extend ResponseMethods

という風に利用されている。HTTPリクエストを出して、戻ってきたレスポンスに対して、、、毎回そのオブジェクトにデコレータとしてのメソッドを混ぜ込んでいる。

いくらなんでも毎回混ぜ込むとかCPUリソースを何だと思ってるんや! という気も一瞬したけど、Hubは人間の使うコマンドラインツールなので、ハッキリ言ってIOの絡まないCPUリソースだけの処理時間なんて誤差もいいところ。インターネットで行ってこいするHTTPリクエストのラウンドトリップが所要時間を決定するので、こういう動的なやり方のパフォーマンス上のデメリットを気にするやつは豚に食われて死ねってことか。

ところで、1行の定義で済むメソッドを1行に書くときに、

class Foo
  def foo; puts "foo" end
end

class Foo
  def foo() puts "foo" end
end

の2つの流儀があり得る。ぼくは前者を使っていた。それは「続けて書いたらRubyのパーザには分かるまい」という配慮だったけど、考えてみれば、元々メソッド呼び出しのときのカッコ・コッカを省略しているという話なのだし、読み手であるプログラマにとっての分かりやすさを考えると、カッコ・コッカを入れたほうがいいような気もしてきた。それによって、Ruby処理系にも分かるし、人間にも分かる。

カッコ・コッカのほうがセミコロンよりも、Ruby特有の事情がないぶんプログラマ一般での通用度が高いような気もするけど、でもセミコロンが文末区切りの明示に使われるのだというのも別に不自然でも分かりづらいわけでもない。この辺は単に取り決めの問題かしら。

ビットシフトでRubyでも2^n倍の掛け算割り算は速くなる

ruby/src/error.c を眺めていて、RangeErrorの以下の例に、おやっと思った。

/*
 *  Document-class: RangeError
 *
 *  Raised when a given numerical value is out of range.
 *
 *     [1, 2, 3].drop(1 << 100)
 *
 *  <em>raises the exception:</em>
 *
 *     RangeError: bignum too big to convert into `long'
 */

RangeErrorはStandardErrorを継承している標準の例外クラスで、範囲外のインデックスを渡して値をフェッチしようとしたときなどにraiseされる。

ruby/src/error.c

  537: VALUE rb_eRangeError;
  538  VALUE rb_eNameError;
  539  VALUE rb_eEncodingError;
  ...
 1744      rb_eIndexError    = rb_define_class("IndexError", rb_eStandardError);
 1745      rb_eKeyError      = rb_define_class("KeyError", rb_eIndexError);
 1746:     rb_eRangeError    = rb_define_class("RangeError", rb_eStandardError);

それはいいんだけど、Array#dropに渡してる (1 << 100) ってなんだ? と、ぎょっとした。って、これはビットシフト演算なのだった。100回左にビットをゴソゴソっと動かすので、2進数で1の後に0が100個続く数になる。10進数だと31桁。つまり、1 << 100 は「明らかにデカイ数」という程度の意味。

Rubyで << は、Arrayに要素をpushするとか、文字列を concat するとか、eval するための文字列を組み立てるときのヒアドキュメントで使うとか、そういうのが多いので面食らってしまった。

そのぐらいRubyでビットシフト演算って、あまり見かけない。そういうことをやる言語ではないような気がする。のだけど、やっぱり2^n倍にする操作をやるのはビットシフトのほうが速い。当たり前か。

require 'benchmark'

p 1 << 1000 == 2**1000

Benchmark.bm do |x|
  x.report { 1_000_000.times { 1 << 1000} }
  x.report { 1_000_000.times { 2**1000} }
end

big_number = 1 << 1000
p (big_number >> 10) == (big_number / 1024)

Benchmark.bm do |x|
  x.report { 1_000_000.times { big_number >> 10 } }
  x.report { 1_000_000.times { big_number / 1024 } }
end
$ ruby -v bitshift.rb
ruby 1.9.3p327 (2012-11-10 revision 37606) [x86_64-darwin11.4.2]
true
       user     system      total        real
   0.560000   0.000000   0.560000 (  0.571141)
   3.180000   0.010000   3.190000 (  3.202365)
true
       user     system      total        real
   0.540000   0.000000   0.540000 (  0.550380)
   0.970000   0.000000   0.970000 (  0.985371)

しかし、Rubyの場合、FIXNUMとBIGNUMがあるし、その辺、ちゃんと面倒みてビットシフト演算もやってくれてるのかなと思って、numeric.cを見たら、

static VALUE
rb_fix_lshift(VALUE x, VALUE y)
{
    long val, width;

    val = NUM2LONG(x);
    if (!FIXNUM_P(y))
	return rb_big_lshift(rb_int2big(val), y);
    width = FIX2LONG(y);
    if (width < 0)
	return fix_rshift(val, (unsigned long)-width);
    return fix_lshift(val, width);
}

と、ちゃんとやってくれてた。FIXNUM_PはLisp命名法則に従ったCRubyのマクロで、与えられたオブジェクトがFIXNUMかどうかを判別する。Rubyで言えば、obj.is_a?(Fixnum) のようなもの。

シフトする数がFIXNUMでない場合には、rb_big_lshift(rb_int2big(val), y); と、BIGNUMのほうのシフト関数に渡している。で、シフトする数(width)がマイナスの場合、左右を反転して、内部APIに渡している。BIGNUMのほうの実装は、

VALUE
rb_big_lshift(VALUE x, VALUE y)
{
    long shift;
    int neg = 0;

    for (;;) {
	if (FIXNUM_P(y)) {
	    shift = FIX2LONG(y);
	    if (shift < 0) {
		neg = 1;
		shift = -shift;
	    }
	    break;
	}
	else if (RB_TYPE_P(y, T_BIGNUM)) {
	    if (!RBIGNUM_SIGN(y)) {
		VALUE t = check_shiftdown(y, x);
		if (!NIL_P(t)) return t;
		neg = 1;
	    }
	    shift = big2ulong(y, "long", TRUE);
	    break;
	}
	y = rb_to_int(y);
    }

    x = neg ? big_rshift(x, shift) : big_lshift(x, shift);
    return bignorm(x);
}

static VALUE
big_lshift(VALUE x, unsigned long shift)
{
    BDIGIT *xds, *zds;
    long s1 = shift/BITSPERDIG;
    int s2 = (int)(shift%BITSPERDIG);
    VALUE z;
    BDIGIT_DBL num = 0;
    long len, i;

    len = RBIGNUM_LEN(x);
    z = bignew(len+s1+1, RBIGNUM_SIGN(x));
    zds = BDIGITS(z);
    for (i=0; i<s1; i++) {
	*zds++ = 0;
    }
    xds = BDIGITS(x);
    for (i=0; i<len; i++) {
	num = num | (BDIGIT_DBL)*xds++<<s2;
	*zds++ = BIGLO(num);
	num = BIGDN(num);
    }
    *zds = BIGLO(num);
    return z;
}

という辺り。big_lshiftが何をやってるのか良く分からん……。numeric.cでシフト幅をwidthと名付けてあるのに、bignum.cのほうでは、shiftと呼んでいるのは不統一感があるけど作者が違うのかな。

numeric.c を眺めていて、

num = 0b1100110010101010

(0..15).each do |i|
  p num[i]
end

=>
0
1
0
1
0
1
0
1
0
0
1
1
0
0
1
1

> [4[0], 4[1], 4[2], 4[4]]
=> [0, 0, 1, 0]

というInteger#[]という記法があるのを知った。

git/GitHubを便利に使うHubを少し読む

git/GitHubを便利に使うRuby製ツールのコマンドラインツール「Hub」のソースコードを少し読んだ。コメントが非常に丁寧だし、小さなツールなので、全体を把握しやすい。いろいろ勉強になる。あれこれランダムにメモしてみる。

featuresの下にあるcucumberのテストケースがあまりにも英文でビビった。CLIツールでもうまく書けば、このぐらい分かりやすいテストが書けるのか。例えば、features/clone.feature にある例は、

Feature: hub clone
  Scenario: Clone a public repo
    When I successfully run `hub clone rtomayko/ronn`
    Then it should clone "git://github.com/rtomayko/ronn.git"
    And there should be no output

  Scenario: Clone a public repo with period in name
    When I successfully run `hub clone hookio/hook.js`
    Then it should clone "git://github.com/hookio/hook.js.git"
    And there should be no output

  Scenario: Clone a public repo that starts with a period
    When I successfully run `hub clone zhuangya/.vim`
    Then it should clone "git://github.com/zhuangya/.vim.git"
    And there should be no output

  Scenario: Clone a public repo with HTTPS
    Given HTTPS is preferred
    When I successfully run `hub clone rtomayko/ronn`
    Then it should clone "https://github.com/rtomayko/ronn.git"
    And there should be no output

という感じ。ちょっとすごすぎるな。HTTPS is preferred ってどうなってるんやとsteps.rbを見たら、

features/steps.rb:
Given /^HTTPS is preferred$/ do
  run_silent %(git config --global hub.protocol https)
end

となっている。なるほどなぁ。こういうツールでcucumberを使うのは少数派という印象も受けてるけど、こうやって取扱説明書のような感じでテストケースを書いていけるのはいいな。

hubは一般的なRubyのライブラリ同様、lib/hub.rbでモジュールを読み込む構成になっている。

├── [        136]  lib
│ ├── [        374]  hub
│ │ ├── [       2856]  args.rb
│ │ ├── [      34064]  commands.rb
│ │ ├── [      12704]  context.rb
│ │ ├── [      11883]  github_api.rb
│ │ ├── [       3023]  json.rb
│ │ ├── [       1937]  runner.rb
│ │ ├── [       2320]  ssh_config.rb
│ │ ├── [       1380]  standalone.rb
│ │ └── [         46]  version.rb

args.rbはARGVをラップして、使いやすくするモジュール。Hub::ArgsはArrayを継承していて、コマンドとして与えられている文字列が、フラグかコマンドか、実行可能コマンドかどうかなんかを判別するロジックを持っている。Hub::Args#before/afterというフックも用意してあって、これは@chainに保存してある。

lib/hub/commands.rbにあるHub::Commandsが、gitをラップする本体で、この冒頭に全体の流れが書いてある。

  # 1. hub is invoked from the command line:
  #    $ hub clone rtomayko/tilt
  #
  # 2. The Runner class is initialized:
  #    >> Hub::Runner.new('clone', 'rtomayko/tilt')
  #
  # 3. The method representing the git subcommand is executed with the
  #    full args:
  #    >> Hub::Commands.clone(['clone', 'rtomayko/tilt'])
  #
  # 4. That method rewrites the args as it sees fit:
  #    >> args[1] = "git://github.com/" + args[1] + ".git"
  #    => "git://github.com/rtomayko/tilt.git"
  #
  # 5. The new args are used to run `git`:
  #    >> exec "git", "clone", "git://github.com/rtomayko/tilt.git"
  #
  # An optional `after` callback can be set. If so, it is run after
  # step 5 (which then performs a `system` call rather than an
  # `exec`). See `Hub::Args` for more information on the `after` callback.

hub/bin/hub は、

#!/usr/bin/env ruby

# hub(1)
# alias git=hub

require 'hub'
Hub::Runner.execute(*ARGV)

で、Runnerは何をするかというと、Argsや用意してCommandモジュールにあるとおり、gitコマンドを実行する。

module Hub
  class Runner
    def initialize(*args)
      @args = Args.new(args)
      Commands.run(@args)
    end

    # Shortcut
    def self.execute(*args)
      new(*args).execute
    end

Rubyではクラスメソッドからインスタンスメソッドを呼ぶことができるので、Runner.executeのように、クラスをインスタンス化してそのまま同名のメソッドを起動するというのはイディオムのようだ。すると、@argsに必要なコマンドが初期化されて、Commands.runが呼ばれる。

Commandsはモジュールとして定義されていて、すべてのメソッドはクラスメソッドとして定義されている。

module Hub
  module Commands
    # We are a blank slate.
    instance_methods.each { |m| undef_method(m) unless m =~ /(^__|send|to\?$)/ }
    extend self

    def run(args)
    def pull_request(args)
    def clone(args)
    def submodule(args)
    def remote(args)
    def fetch(args)
    def checkout(args)
    def merge(args)
    def cherry_pick(args)
    def am(args)
    alias_method :apply, :am
    def init(args)
    def fork(args)
    def create(args)
    def push(args)
    def browse(args)
  :
  :

Commandsモジュールは単なるメソッドの置き場となるコンテキストの役割りを果たしているだけなので、Objectにある余計なメソッドを undef している。Rubyでは何でもオブジェクト。ModuleはObjectを継承したクラスなので、Commandsモジュールには、clone、dup、class、freezeなどのインスタンスメソッドが最初から含まれている。

こういうテクニックは「ブランクスレート」(何も書かれていない白板)と呼ばれ、イディオムになっているので、Ruby 1.9からはBasicObjectがそれを提供するのようになっている。最近は、1.9で導入された処理系の機能を使う場合、バージョン判定をしてネイティブがあればそれを、そうでなければその場で定義してしまうというコードを良く見る。BasicObjectを使えばいいのに。好みの問題なのだろうか。

実際にgitのサブコマンドとObjectのインスタンスメソッドは名前の衝突があるのだろうかと思って、

module Commands
  puts "#{instance_methods}"
  instance_methods.each do |m|
    unless m =~ /(^__|send|to\?$)/
      puts "undef: #{m}"
      undef_method(m)
    end
  end
end

と書いて実行してみて気が付いた。このコードは何も出力しない。そうだ、moduleなんだからインスタンスメソッドなんてないのだ。試しに1行目のmoduleをclassに変えると、たくさんメソッド定義が削除される様子が分かる。

あれ? ということは、module Commandsというのは、以前に class Commands だったのだろうか? ブランクスレートというのは消し忘れか何かか? と思って git の歴史を見てみたけど、そうでもない。試しに、blank slateの1行をコメントアウトしてみたら、ちゃんとテストが通る。むむむ? もしかして勘違い? Hub作者のdefunktさんって、GitHubの共同創業者のスターエンジニアだし、どっちかいうとぼくの勘違いの可能性が高そうだけど。

ともあれ、Commandsに定義されているコマンドは、例えば以下の通り。git cloneに相当するコマンドの定義は:

    def clone(args)
      ssh = args.delete('-p')
      has_values = /^(--(upload-pack|template|depth|origin|branch|reference)|-[ubo])$/

      idx = 1
      while idx < args.length
        arg = args[idx]
        if arg.index('-') == 0
          idx += 1 if arg =~ has_values
        else
          # $ hub clone rtomayko/tilt
          # $ hub clone tilt
          if arg =~ NAME_WITH_OWNER_RE and !File.directory?(arg)
            name, owner = arg, nil
            owner, name = name.split('/', 2) if name.index('/')
            project = github_project(name, owner || github_user)
            ssh ||= args[0] != 'submodule' && project.owner == github_user(project.host) { }
            args[idx] = project.git_url(:private => ssh, :https => https_protocol?)
          end
          break
        end
        idx += 1
      end
    end

ほかのhubのコードに比べると、妙に泥臭いような感じもある。ssh = args.delete('-p') のような Array#delete 使い方がなるほどなと思った。

Commandsに定義されているコマンドのメソッドは、Commands.runから起動される。Argsオブジェクトに入ってるコマンド名や、コマンドのエイリアスを展開して、send(cmd, args) として対応コマンドを呼び出している。

    def run(args)
    :
      cmd = args[0]
      if expanded_args = expand_alias(cmd)
        cmd = expanded_args[0]
        expanded_args.concat args[1..-1]
      end
      # git commands can have dashes
      cmd = cmd.gsub(/(\w)-/, '\1_')
      if method_defined?(cmd) and cmd != 'run'
        args.replace expanded_args if expanded_args
        send(cmd, args)
      end

    :
    :
    end

というのが初期化とコマンド実行の流れ。

うまいこと処理の流れを作ってコードをまとめてるなぁと思う。これぐらいの構造をスラスラ書けると楽しいだろうなと思うんだけど、それはそれでちょっと勘違いなのかも。gitでfirst commitを引っ張りだして眺めてみたら、Hubは最初はわずか60行のスクリプトからスタートしてるのだな。オーガニックにコードが育ってきたってことだろうか。今までやったことないけど、gitを使って、コミットを過去から現在に渡って再現してみようかな。

git/GitHubを便利に使うHubを少し読む(2)

git/GitHubを便利に使うRuby製ツールのコマンドラインツール「Hub」のソースコードを少し読んだので、あれこれメモ。git/GitHubを便利に使うHubを少し読むの続き。

GitHubAPIをラップする lib/hub/github_api.rb は、クラス定義っぽいところを抜き出すと以下のような感じ。

module Hub
  class GitHubAPI
    module Exceptions
    module HttpMethods
      module ResponseMethods
    module OAuth
    include HttpMethods
    include OAuth
    class FileStore
    class Configuration

FileStoreは設定をファイルに落としたり、ロードしたりするモジュールで、Configurationは設定を扱うクラス。この2つは別ファイルに分けて良さそうな感じもあるけど、汎用のクラスではなくて、GitHubAPI用のものなので、1つのファイルに入ってるっぽい。長くないし。サブディレクトリを切って、そこに入れるという発想はあるのだろうけど、1つのファイルにどこまでクラスを含むかという粒度は好みで決まるものらしく、最近のRailsは割と細かくファイルを分ける傾向にあるような気がしてるけど、Hubみたいなツールでは「いいよ、入れちゃえよ」という傾向があるような気もしなくもない。Javaなんかだと、こういうのってインナークラスとかいうヤツでやるのかしら。

GitHubAPIの初期化のコメントで、ちょっとハッとした。

module Hub
  class GitHubAPI
    attr_reader :config, :oauth_app_url

    # Public: Create a new API client instance
    #
    # Options:
    # - config: an object that implements:
    #   - username(host)
    #   - api_token(host, user)
    #   - password(host, user)
    #   - oauth_token(host, user)
    def initialize config, options
      @config = config
      @oauth_app_url = options.fetch(:app_url)
    end

Hub::GitHubAPI.new(config, options) で初期化するわけだけど、ここで渡す config は、「以下を実装したオブジェクト」だというコメントがついている。実際、Configurationクラスは、

module Hub
  class GitHubAPI
    class Configuration
      def initialize store
      def normalize_host host
      def username host
      def api_token host, user
      def password host, user
      def oauth_token host, user, &block
      def prompt what
      def prompt_password host, user
      def askpass
      def proxy_uri(with_ssl)

となっている。何も不思議なところはないけど、「実装したクラス」とか「実装したクラスのインスタンス」ではなく「実装したオブジェクト」とコメントにあるのがちょっとおもしろいと思った。ここで渡す config って、Configuration のインスタンスではあるけど、コメントにあるとおりのメソッドに反応しさえすれば、例えば、BasicObjectを継承しただけのシングルトンでも構わないわけだ。いやいや、o = Object.new; o.extend SomeMethods とかして、本当にオブジェクトでもいいんだ。この辺が、ダックタイピングぽい発想なのかしら。

このあいだ知って、ちょっと驚いたけど、RubyのStringIOクラスとIOクラスは継承関係がない。両者のAPIはソックリで、インスタンスメソッドのインターセクションを取ってみると、

> sio = StringIO.instance_methods(false)
> io = IO.instance_methods(false)
> sio & io
=> [:reopen, :lineno, :lineno=, :binmode, :close,
 :close_read, :close_write, :closed?, :eof, :eof?, :fcntl,
 :flush, :fsync, :pos, :pos=, :rewind, :seek, :sync, :sync=,
 :tell, :each, :each_line, :lines, :each_byte, :bytes,
 :each_char, :chars, :each_codepoint, :codepoints, :getc,
 :ungetc, :ungetbyte, :getbyte, :gets, :readlines, :read,
 :write, :putc, :isatty, :tty?, :pid, :fileno,
 :external_encoding, :internal_encoding, :set_encoding]

という感じ。seekもできるし、posでオフセットを移動できる。ところが、StringIOはCRubyの ext/stringio/stringio.c で以下のように定義されている。

/*
 * Pseudo I/O on String object.
 */
void
Init_stringio()
{
    VALUE StringIO = rb_define_class("StringIO", rb_cData);

    rb_include_module(StringIO, rb_mEnumerable);
    rb_define_alloc_func(StringIO, strio_s_allocate);
    rb_define_singleton_method(StringIO, "open", strio_s_open, -1);
    rb_define_method(StringIO, "initialize", strio_initialize, -1);
    rb_define_method(StringIO, "initialize_copy", strio_copy, 1);
    rb_define_method(StringIO, "reopen", strio_reopen, -1);

「rb_define_class("StringIO", rb_cData);」ということで、「rb_define_class("StringIO", rb_cIO)」となっていない。StringIOはIOクラスを継承していない。

rb_cDataは、この掲示板の書き込みにあるMatz御大らしき人の説明によると、CのポインタをラップするときにRubyの拡張モジュールで使うもの、らしい。

ともあれ、StringIOは、いかにもIOを継承していて良さそうなのに継承していない。

なんでかというと、StringIOはIOから継承するべき実装が何もないから。というのがたぶん答え。Javaのようにインターフェイスを作って、それを実装するようなカチッとした言語とRubyが大きく異るのは、

class Base
  def initialize(foo)
    @foo = foo
  end

  def meth1
    raise "not implemented"
  end

  def meth2
    raise "not implemented"
  end
end

class Hoge < Base
  def meth1
    something
  end

  def meth2
    something else
  end
end

class Foo < Base
  def meth1
    something
  end

  def meth2
    something else
  end
end

というようなことを決してやらない、というところ。基底クラスが空っぽの場合、そんなもん定義するだけ時間の無駄だし、無駄にコードが増えて仕様変更に弱くなるだけ、というゆるい感じがRubyの流儀らしい。反応すべきメソッドが揃ってればそれでええやん、と。だから、IOとStingIOも別に継承関係なんて要らない。一方のインスタンスを期待するコードは他方のインスタンスが渡されても、だいたい動くはず。

そんなゆるいやり方じゃ不安だと、is_a? とか、kind_of? とかして受け取った引数のクラスというか型を調べたくなるんだけど、それは静的言語のやり方を動的言語に当てはめて、動的言語の良さを台無しにする行為。動的言語の良さは何も int や char といった宣言をせずに済むというような話だけじゃなくて、こういうゆるい結合によって書かずに済むコード量にある。というようなことを、Eloquent RubyのChapter 8あたりでRuss Olsenが書いている。

動的言語はパフォーマンスを犠牲にしてまで型チェックをやらないのだから、自前でそんなチェックをやるぐらいなら静的言語を使えばいいということなんだろう。

Ruby標準ライブラリのDigestでの const_missing の用法

Digestには何があったかなと思って、ruby/ext/digestを見てみたら、lib/digest.rbに以下のようなコードあった。Rubyハッシュ関数として、MD5SHA1、SHA2、RDM160というのなんかをサポートしてる。BubbleBabbleというマイナーなものも入ってるらしい。で、これらはCでモジュール化されていて、呼ばれたときに初めてロードされる。というのが、以下のコード。Digetst::Hoge.newなどとすると、libにHogeが入っていれば、require される。

require 'digest.so'

module Digest
  def self.const_missing(name) # :nodoc:
    case name
    when :SHA256, :SHA384, :SHA512
      lib = 'digest/sha2.so'
    else
      lib = File.join('digest', name.to_s.downcase)
    end

    begin
      require lib
    rescue LoadError
      raise LoadError, "library not found for class Digest::#{name} -- #{lib}", caller(1)
    end
    unless Digest.const_defined?(name)
      raise NameError, "uninitialized constant Digest::#{name}", caller(1)
    end
    Digest.const_get(name)
  end

なんだか分からなかったのは、::Digest::Class というクラス。

module Digest
  :
  class ::Digest::Class
    # creates a digest object and reads a given file, _name_.
    #
    #   p Digest::SHA256.file("X11R6.8.2-src.tar.bz2").hexdigest
    #   # => "f02e3c85572dc9ad7cb77c2a638e3be24cc1b5bea9fdbb0b0299c9668475c534"
    def self.file(name)
      new.file(name)
    end

    # Returns the base64 encoded hash value of a given _string_.  The
    # return value is properly padded with '=' and contains no line
    # feeds.
    def self.base64digest(str, *args)
      [digest(str, *args)].pack('m0')
    end
  end

コメントを読んでもピンと来なかったけど、これは、

module Digest
  class SHA2 < Digest::Class

などと継承が前提の、ほとんど中身のないクラスだった。クラスメソッドとして、Digest.fileを定義して、それをインスタンスメソッドに渡すような役割りをしている。DigestはCとRubyが入り混じっていて、ぼくには読むのが難しい。