極めよRuby道

第3回 メソッド間の依存関係

後藤謙太郎 <URL:http://www.notwork.org/~gotoken/mag/cmagazine/>

註: この文書は『C MAGAZINE』2000年10月号 に掲載された記事の元となるものに手を加えたものです.

記事中のプログラムを一つずつファイルにしたものもあります(→list)

中田さんのおかげでNetscapeでも読めるようになりましたが,まだちょっとカッコ悪いです。「CSSの書き方がなっとらん、こう書け」というお便りは歓迎します。

間違いを見つけたら,gotoken@notwork.org宛に御連絡くださると喜びます。

Copyright(c) 2000 by GOTO Kentaro. All rights reserved.


風が吹けば…, メソッドの動的な依存関係, メソッドと言うシンボル, メソッドの依存関係一覧, 型の変換 ( to_a, to_s, 文字列とみなす「to_str」, 人に読める形にする「inspect」 ), 文法要素 ( 等しい「==」, マッチ「=~」, ), 数値の周辺 ( coerce, zero?, 足す「+」, 引く「-」, マイナス「-@」, 商「/」と余り「%」, induced_from, prec ), メタプログラミング ( inherited, method_added, singleton_method_added, backtrace, set_backtrace, extend_object, append_features ), いろいろ ( 場合分け「===」, <=>, >, <, <=, >=, <=>, succ, each ), Marshal ( _dump, _load ), まとめ, 謝辞, 参考資料,


風が吹けば…

「風が吹けば桶屋が儲かる」という小噺を耳にした方は多いことと思います。これは思いがけない因果の連鎖の妙で、風が吹けば塵が飛ぶ→塵が飛ぶと目に入る→盲が増える→盲の仕事は三味線の門づけ→三味線の材料は猫→猫が減る→ネズミが増える→ネズミは桶をかじる→桶屋が儲かる、という流れになっています。三味線の門づけと言われてもピンと来ませんが、意外な連鎖には意外な登場人物がいてもおかしくは無いのかも知れません。

門づけは極端な例としても、世の中には案外知られていない連鎖というはあるものです。たとえば僕はFreeBSDを使っていますが、FreeBSDユーザによく使われているCVSupというツール群はCVSリポジトリを配布、取得するためのものですが、このCVSupはModula-3という言語で書かれています。Modula-3といえば Rubyのお友だちのPythonに影響を与えた言語だけれども、Modula-3でプログラムしたことのある人はそんなに多くないと邪推します。ところがFreeBSDで「cd /usr/ports/net/cvsup; make install」すると依存関係によりModula-3 の処理系のひとつPM3もインストールされます。そんなわけでCVSupを使っている人のマシンにはModula-3がインストールされているんですね。

考えてみればCVS自体も依存関係を表すものだし、FreeBSDのportsシステム*1もそうだし、Rubyをコンパイルするときに使うmakeやconfigureといった道具もやはり依存関係を扱うための道具です。このように、依存関係を記述する、あるいは調べることは計算機を利用する上で不可欠な技術の一つと言えます。

メソッドの動的な依存関係

Rubyのようなオブジェクト指向言語ではメソッドが実行時に決定されることが大きな特徴の一つです。いいかえれば、あるメソッドが実行されるときに実際にどこで定義されたメソッドが利用されるかは実行されるまでは原理的には必ずしも分からないわけです。たとえば、前回Enumerableのご利益を紹介しました。簡単に復習するとeachというメソッドを持ったクラスに Enumerableをインクルードすれば、2ダース程のメソッドが利用できるようになるという極めて便利な物です。ところが、Enumerableの中で使われているeachは実際はどこのeachなのかは決まっていません。逆に言えば、そうなっているからこそEnumerableは有用なのです。

このほかにも、例えばインスタンス間で比較が出来るクラスには万能な比較演算子「<=>」だけ定義しておいてComaparableをインクルードしておけば残りの比較「<」「<=」…等を利用できます。 Comparableで使われている<=>もどこで定義されているものなのかは「<」などを使ってみるまではやっぱり決まっていません。

もっと混み入った場合もあります。Enumerableで定義されるメソッドの一つsortは要素間に順序が定義されていることが期待されますから、 Enumerable#sort はレシーバ*2eachを使ってEnumerable#to_aを実行し*3、結果的にレシー バの「要素」の<=>に依存しているといえます。また、先週登場した例にはデバッグプリンタ「p」もありました。このメソッドは引数を inspectした結果をその場でプリントするのでしたが、この場合p は引数のinspectに依存しているわけです。ところがこのinspect はそのオブジェクトに固有のinspectが定義されていない場合、デフォルトではto_sが使われるようになっています。

いずれの場合もどこで定義されたメソッドなのかは実行時に初めて決まるのです。もっとも、こう言うとRubyってなんだかややこしい言語なのね、と思われてしまいそうですが、本当のところは注意深くデザインされているおかげで、けったいなことはまず起こらず、意識せずともその動的な仕組みを極々自然に利用出来るようになっていますから、心配しないで下さいね。『The Pragmatic Programmer』[HT99]の著者の一人である Andrew Hunt さんは「Ruby is terrific at honoring the "Princple of Least Surprise"」評しているくらいです[RT]

メソッドと言うシンボル

Rubyにおいて、実行時に初めて依存関係が分かるという性質を可能にしているのは、関係がメソッド名というシンボルによって規定されるというメカニズムです。インタプリタ内部では、固定された機能を提供するためにメソッド名を使わず直接Cの関数を呼び出している部分の方が多いのですが、ユーザに変更を解放するためにわざわざシンボルで呼び出している箇所もかなりあります。上で挙げた例はすべてそうです。

さてさて、いささか前振りが長くなってしまいましたが、このようなメソッド間の依存関係を眺めてみようというのが今回のお題です。冒頭の小噺は落語なので意外な落ちが来ればこそ面白いのですが、プログラミングの場面では「風が吹けば桶屋が儲かる」ようなことばかりでは困ります。逆にRubyの提供する依存関係を知っておくと実装が簡単になることも少なくありません。

メソッドの依存関係一覧

以下では使われるメソッドを見出しとし、どのメソッドに使われるかを見出しの直後に書いています。一部メソッドでない用途も含まれています。使うメソッドを見出しとしなかったのは、そのメソッドにどんな意味があるかを示すためです。意味といっても仕様書ではなく国語辞典的な説明を狙っています。凡例は挙げませんので読み方は推測して下さい。

型の変換

まず、比較的分かりやすい型変換をするものから示しましょう。

to_a

呼び出すメソッド*4

Array#concat, Array#+, Array#replace <引数>, 多重代入 <右辺>, Array <引数>

配列を返します。→each

to_s

呼び出すメソッド

inspect <デフォルト>, display, puts, format, print, String#%, String <引数>

文字列にします。IOなどに出力することが主な目的です。

文字列とみなす「to_str」

文字列として振舞う必要のあるときにこのメソッドが呼ばれます。このメソッドが用意された背景は、暗黙の型変換をする場合をユーザに指定させることです。以前のRubyでは、文字列にしてしまう傾向があったのですが、いまでは、 to_str が無いかぎり暗黙の型変換をほとんどすることはありません。

人に読める形にする「inspect」

呼び出すメソッド

p <引数>, 各種エラーメッセージ

文法要素

混乱を避けるためにRubyでは != と !~ が定義できないようになっています。

等しい「==」

呼び出すメソッド

!= <レシーバ>, === <デフォルト>

マッチ「=~」

呼び出すメソッド

!~

数値の周辺

coerce

呼び出すメソッド

Numeric#+, -, *, /, %, **, ^, &, |, -@ <引数>

型を矯正します。Smalltalkに由来するcoerceシステムはは数値クラスをRuby のなかでも独特の世界に仕立てています。Rubyの演算子はメソッド呼び出しの特別な文法として扱われ、例えば「a+b」は「a.+(b)」という非直観的な書き方をしない代わりに算術の記法に似せたものです。このため演算子を再定義したり新たな数値クラスを定義することも容易になっていますが、新たなクラスを定義した場合、そのクラスのオブジェクトを既存クラスの演算子が理解できないという問題の解決として採用されているのが coerce です。

たとえば、単項のマイナス Numeric#-@ を例にとると、-@の定義はリスト1とほぼ等価です。すなわち、零に相当するオブジェクトzeroと自分の型を適切な型に変換したmeを作り、その差を計算しています。

-- List1 numuminus.rb

  class Numeric
    def -@
      zero, me = coerce(0)
      zero - me
    end
  end

このように、coerce(x) の値はちょうど2個の要素からなる配列であることが必要で、その値を [y,n] とすると、ynxselfをそれぞれ適切なクラスに変換したものでなければなりません。詳しくは、標準添付のrational.rbなどをごらん下さい。-@を使う典型例はList2に示します。

zero?

呼び出すメソッド

Numeric#nonzero?

その数値が零かどうかをtrue/falseで返します。一方、nonzero? は !x.zero? とは違い、xが零でないときx自身を返します。 nonzero? はソートのために考えられたのですが、Array#<=> が再帰的な辞書式順序を返すになっている今となってはちょっと出番がないかも知れません。考古学に興味のあるひとは[ruby-list]の過去ログを検索するといろいろ出てきます。

足す「+」

呼び出すメソッド

Numeric#succ <レシーバ>, upto, step, times <イテレータ変数>, String#sum <内部処理>

足し算です。呼び出す側では 1 との足し算も行いますので Integer との足し算をサポートしておかなければなりません。

引く「-」

呼び出すメソッド

Numeric#-@ <レシーバ>, downto <イテレータ変数>, String#sum <内部処理>

ここでは -@ がどのように使うかを示します。

-- List2 zero.rb

  class Bit < Numeric
    def initialize(n = 0)
      if n.to_i == 0
        @n = 0
      elsif n.to_i == 1
        @n = 1
      else
        raise RangeError, "#{n} for 0 or 1"
      end 
    end

    attr_reader :n
    protected :n

    def coerce(other)
      [Bit.new(other), self]
    end

    def -(x)
      case x
      when Bit
        y = self
      when Integer
        x,y = self.coerce(x)
      else
        x,y = x.corece(self)
      end

      Bit.new((x.n+y.n)%2)
    end

    def inspect
      "Bit(#{@n})"
    end
  end

  p -Bit.new(1)
  p -Bit.new(0)

マイナス「-@」

呼び出すメソッド

Numeric#abs <レシーバ>

単項の演算子です。リテラルで -1 と書いた時は呼ばれませんが、-a と書くと a の @- が呼ばれます。絶対値を返す abs は零との比較を伴う ため < 0 も使います。

商「/」と余り「%」

呼び出すメソッド

Numeric#divmod <レシーバ>, String#sum <内部処理>

Numeric#divmod の定義は以下のようなものです

class Numeric
  def divmod(other)
    div = self/other
    if div.is_a? Float
      d = div.floor
      div = d if div > d
    end
    return [div, self%other]
  end
end

induced_from

呼び出すメソッド

Precision#prec <引数>

たとえば Float::induced_from(x)x を Float に変換します。組み込みの数値クラスだけなら to_f などを使えばすむのですが、新たに作られた数値クラス間と組み込み数値クラスの間で精度変換をするためのプロトコルとして提供されます。詳しくは次の prec を参照して下さい。

prec

呼び出すメソッド

Precision#prec_i, Precision#prec_f <レシーバ>

新しい精度を持つ数値クラス Foo を作ったとしましょう。この時、 Foo がクラスメソッド Foo::induced_from を備えていれば、 Precision をインクルードすることで 5.prec(Foo) で、整数 5 を Foo に変換できます。

メタプログラミング

Rubyではクラス定義も実行文なので、継承やメソッド定義もイベントとしてとらえることが出来ます。また、includeextend はディスパッチになっていて、挙動を変えることが出来ます。これらの機能の多くはRubyの対話環境である irb の開発から要求されたものです。

inherited

呼び出すメソッド <スーパークラス>

(サブクラス定義), Class::new <引数>

既存のクラス Foo から Bar を継承したとき、Foo::inherited があれば Bar を引数として呼び出されるフックです。なお Class::new の引数はスーパークラスです。List3は簡単なデモです。Barが出力されます。

-- List3 inherited.rb

  class Foo
    def Foo::inherited(sub)
      p sub
    end
  end

  class Bar < Foo  # ここで Foo::inherited(Bar) が呼ばれる
    p "defining"
  end

method_added

呼び出すメソッド

(メソッド定義) <クラス>

メソッドが定義された時にその定義されるメソッド名のシンボルを引数として呼ばれるフックです。List4は "foo""bar" を出力します。

-- List4 method_added.rb

  class Foo
    def Foo.method_added(mid)
      p mid.id2name
    end
  end

  class Bar < Foo; end

  class Foo
    def foo; end
  end

  class Bar
    def bar; end
  end

singleton_method_added

呼び出すメソッド

(特異メソッド定義) <オブジェクト>

method_added と似ていますが、特異メソッド定義のときに呼び出されるメソッドです。引数はそのメソッド名です。

backtrace

呼び出すメソッド

(例外表示等)

backtrace は Exception クラスのメソッドで、デフォルトでは例外が起こったときに使われるエラーメッセージ中のバックトレース情報として用いられます。次の set_backtrace と併せて使うのが有用でしょう。

set_backtrace

呼び出すメソッド

(例外の発生)

例外オブジェクトのバックトレース情報を設定します。文字列の配列を引数に与えなくてはなりません。

extend_object

呼び出すメソッド

Object.extend <引数>

あるオブジェクトに特異メソッドとしてモジュールの機能を追加するのが extend ですが、この extend の実体が Module#extend_object です。extend_objectの引数にはextendのレシーバが渡されます。

append_features

呼び出すメソッド

Module#include <引数>

include ではクラスメソッドを取り込めませんが、そこを補う方法として、append_features を利用できます。また、実行環境に応じて実際にインクルードする対象を変えるのにも使うことが出来ます。 append_featuresの引数にはincludeのレシーバが渡されます。

-- List5 append_features.rb: newに別名new2を与える

  module New2
    def New2.append_features(mod)
      mod.instance_eval <<-end
        alias new2 new
      end
    end
  end

  class Foo
    include New2
    def initialize(*param)
      @param = param
    end
  end

  p Foo.new2(1)  # Foo.new(1) と等価

いろいろ

特に分類できない物をまとめました

場合分け「===」

呼び出すメソッド

(case) <条件>, Enumerable#grep <引数>

Ruby の case は任意のクラスのオブジェクトを検査することが出来、 when で始まる条件にオブジェクト指定しますが、この検査は === を使っています。例えば、次の case では arg がどのクラスに対して is_a? が成り立つかを調べるものです。

case arg
when Integer or String
  ....
when Float
  ....
else
  ....
end

<=>

呼び出すメソッド

Array#<=>, sort <要素>, Comparable#<, <=, >, >=, ==, between <レシーバ>, Range#new <引数>

「大小」比較の結果を整数で返すメソッドです。ab の「大小」をくらべて、aが大きい時は正、b が大きい時は負、等しい時は 0 を返さなければなりません。

>

呼び出すメソッド

Integer#upto <イテレータ変数>, downto <ステップ>, Range#length <first>

<

呼び出すメソッド

Numeric#abs <レシーバ>, Integer#downto, times <イテレータ変数>, Range#each <イテレータ変数>

<=

呼び出すメソッド

Range#===

>=

呼び出すメソッド

Range#===

succ

呼び出すメソッド

Range#each <イテレータ変数>, String#upto

succ は「次」の値を返すものです。例えば、3.succ は 4、"2z" の次は"3a"、と言った具合です。こうして、範囲を表すクラス Range のオブジェクトを構成する両端には、<, >, <=, >=, <=>, succ が必要なことが分かりました。 Complarable であり、かつ succ を持てばば十分ですね。

each

呼び出すメソッド

Enumerale の全てのメソッド <レシーバ>

each については前回の記事を参照して下さい。

Marshal

モジュール Marshal は、オブジェクトをIOか文字列にダンプし、またロードするという機能を提供しています。「a = []; a.push a」で作られた循環参照を含むaにも対応しているのでときに便利です。このようなことを実現するにはクラスの内部構造を知っている必要があるのですが、組み込みでないクラスにも対応できるように、ディスパッチを用意しています。

_dump

呼び出すメソッド

Marshal::dump <引数>

Marshal::dump(obj)obj_dump を持っていればその結果をダンプします。次の _load と対になっている必要があるます。

_load

呼び出すメソッド

Marshal::load <引数>

_dumpの逆です。_dump をするオブジェクトはその出力から元のオブジェクトを再現出来るような _load を持っていなければなりません。

まとめ

Rubyはオブジェクトごとにメソッドを定義出来るため、かなり細かく挙動を変更できるようになっています。自然なものしか用意されていないので、依存関係に気づかないことが多いでしょう。知らなくても構わない物も多くありますが、この依存関係に気づくと大変便利さに出来ていることが分かると思います。

冒頭でも述べたように、メソッド名で依存関係が規定されています。そのためユーザはメソッドの命名をなおざりにせず、なるたけ誤解のないような名前をつけることが良いとされています。新しい機能を提案しても、メソッドの名前が決まらないために議論が中断することもあるくらいです。メソッドの改名の提案もしばしばみかけます。これも意外性を減らそうとするRubyのポリシーの現れでしょう。こうして名前が機能を予感させ、メソッド名で関係を規定し、実行が意味を確定するというやり方になんだか深いものを感じるのは僕だけでしょうか。

謝辞

今回の原稿を書くに当たって、Rubyのソースコードを探検するために、 [CFLOW][CSCOPE]というツールを利用しました。これらはCのプログラム内の関数の依存関係を調べるツールですので知っておくと役立つことがあるかも知れません。これらの存在を教えてくれた後藤裕蔵さんに感謝します。今回は使いませんでしたが、似たような道具に[CXREF][GLOBAL]などもあります。また、ソースの追跡方法を教えて下さったまつもとゆきひろさんありがとうございます。せっかく教えてもらったのに追跡が足らなかったらごめんなさい。

参考資料

[CFLOW]

例えば、 <URL:ftp://ftp.u-aizu.ac.jp/pub/lang/netsw/C/Tools/cflow/cflow-2.0.tar.gz>

[CSCOPE]

<URL:http://cscope.sourceforge.net>

[CXREF]

<URL:http://www.gedanken.demon.co.uk/cxref/>

[GLOBAL]

<URL:http://www.tamacom.com/global/>

[HT99]

Andew Hunt and Dave Thomas, ``The Pragmatic Programmer'', Addison-Wesley (1999), <URL:http://www.pragmaticprogrammer.com/ppbook/index.html>, ISBN0-201-61622-X

[RT]

``Ruby Testimonial'', <URL:http://www.ruby-lang.org/en/testimony.html>

[ruby-list]

<URL:http://blade.nagaokaut.ac.jp/ruby/ruby-list/index.shtml>


[Ruby] Rubyどうでしょう
著者: 後藤謙太郎
御意見,御感想,御批判の宛先: gotoken@notwork.org


*1 アプリケーションの依存関係に関する巨大なMakefileのようなもの。ソースの取得も自動でインストールが楽チンなのはメンテナのみなさんのおかげ
*2実行時にメソッドを解釈する実際のオブジェクト
*3 Ruby 1.6.x では多重代入は to_ary で行なわれます
*4 Ruby 1.6.x では多重代入は to_ary で行なわれます