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が書いている。

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