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」のソースコードを少し読んだので、あれこれメモ。これとこれの続き。
GitHubのAPIをラップする 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を少し読むの続き。
GitHubのAPIをラップする 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はハッシュ関数として、MD5、SHA1、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が入り混じっていて、ぼくには読むのが難しい。