極めよRuby道

第9回 モジュールとクラスの使い方

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

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

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

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

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


コードの読みやすさとは?( 慣習の重要性, あとで自分が分かるように), モジュールの役割( 名前空間としてのモジュール, Mix-inとしてのモジュール, 特異メソッドとモジュール, 余談: 特異メソッドと特異クラス, mix-in でクラスメソッドを追加, モジュールとパターン), Classというクラス( 動的なクラス生成, ArrayOf() は名前をつけるべき?), まとめ, 謝辞, 参考資料


コードの読みやすさとは?

オフラインでRubyの話になったときにときたま、難しいコードがあるよね、という話になります。難しいというのは読むのが難しいという意味です。Rubyは Perlと比較して暗号っぽくなりにくいということを一つのウリにしているのですが、コードの読みやすさは言語だけでは強制できないものです。ではコードの読みやすさとはなんでしょう?

古い例を挙げるとCOBOLという言語が出た当初は、これでプログラミング言語を勉強する必要がなくなった、なんてことがいわれたそうです。英語が理解できればCOBOLは読み書きできるというのがこの言葉のいわんとするところなのですが、COBOLのメンテナンスコストの高さがソフトウェア工学を発展させたといっても過言でないくらい読みにくい上に書きにくいことは周知の事実です。もっとも、COBOLが大変だったのは別に自然言語に似ていたせいではないでしょう、たぶん。というか英語に似てると思いますか?

まぁCOBOLは極端だとしても、たとえばRubyの良きお手本でありライバルでもあるPythonでも似たようなことは起こります。Pythonは自然と読みやすいコードになるように設計されているといわれていて、実際その抑制の気いた文法ではアルゴリズムがコードを決定することが少なくないのも事実です。スタイルの重要性を学ばせるために*1最初の言語として学生にPythonを使わせたいという筆者の友人もいます。しかし、プログラミング言語というものは強力であれば強力であるだけ何でもできるわけで、文法や基本機能だけでコードの読みやすさを支配できるわけではありません。Pythonでも読みにくいコードというのはやっぱりあります。スタイルや基本機能の充実も重要だけれどもそれを守っているからといってそれだけでコードが読みやすいわけではなさそうです。

慣習の重要性

PythonにしろRubyにしろそれは当てはまるわけで、どんな言語を使っても、いや、むしろマトモな言語でこそ読みにくいコードを書くことが可能です。スタイルを強制することである程度の読みやすさを確保することはできますが、プログラムの読みやすさはそれだけではありません。要すれば、

が問われているのです。これは会話の中で人の言っていることを把握するのに似ています。どうも、プログラミング言語も純粋な機械への命令ではなく、人に考えを伝えるメディアという意味で「コミュニケーションの道具」であることにも読みやすさ/読みにくさの起源はあるようです。いいかえれば読みやすさというものはある種の慣習に沿っているかどうかで決まるような相対的なものであって、世の中の慣習と独立に読みやすさを数学的に定義するようなことはできません。

余談ですが、実は数学自体もそうです。計算機の利用におけるプログラムは数学では証明に相当するといってもよいのですが、証明にはプログラム同様、読みやすい証明と読みにくい証明があり、その読みやすさもスタイルに起因するものだけでなくロジックの自然さもあります。つまりロジックの建て方やその並べ方ですら慣習に支配されているといえます。そこで数学者は日々、他人の書いた証明を読むことで証明の慣習を知り、それを実践しているんですね。数学の論文も正しければそれで良いわけではないのです。言語ゲーム*2 という考え方は認めたくありませんが、およそ言語と名のつくものはその利用によって規定される側面を確かに持っているようです。

あとで自分が分かるように

話を戻すと、プログラマが読みやすいコードを書くにはプログラマの習慣を無視できないということになります。言い替えれば既存のコードを読む必要があるということです。明示的ではないもののRubyにはRubyのやりかたというのがやっぱりあるようです。いっぽうでRubyはプログラミングの負担を軽減する多くの機能から成り立っていまするので、これらの機能を使いまくることでコードをとても短くしたり、自分的にカッコいいコーディング法を開発することもできます。しかし、まずはストレートな使い方というものを押えておかねば、あなたのコードはどんどん慣習から外れたものになっていってしまうでしょう。その結果、読みにくいということになるわけです。

さて、Rubyではクラスやモジュールも一つのオブジェクトです。このことを使った動的なプログラミングのための道具も多く揃っており、Rubyのプログラミングの最も楽しい部分の一つだと感じます。けれどもその濫用はときには難解なコードを導きます。そこで今回はクラスとモジュールの役割とその使い方をみていきましょう。典型的な使い方は目的を正しく理解することで身につけることができるものです。また使い方の良い悪いをいくつか議論します。

モジュールの役割

まずは基本から。Rubyでモジュールの果たす役割は、大きく分けて2つあります。1つは名前空間の生成です。定数や関数の名前がぶつかりそうなときその名前をひとまとめにして遮蔽する効果があります。もう1つの役割は、mix-in のためです。多重継承を意図的にサポートしていないRubyのような言語では必須の機能で、1つのクラスに複数のモジュールを組み込むことができます。

名前空間としてのモジュール

特に定数のように名前がぶつかるかも知れないものを分けておくのには長い名前を作るよりも名前空間を分ける方が良いことが多いでしょう。また、名前空間を分けておけば、あとでその名前空間を別の場所に展開できます。典型的な例としては File::Constants があります。Fileはクラスですが、 ClassModuleのサブクラスなので、この場合は名前空間としても使われています。そして名前空間としての File のなかに Constants というモジュールが用意されています。このモジュールは Fileオブジェクトのメソッドで用いられる各種の定数をまとめてあるところです。その名前と値の一覧は次のようにして表示できます。

k = File::Constants
k.constants.each do |c|
  p [c, k.const_get(c)]
end

Module#constants はそのモジュールの定数名を文字列として格納した配列を返すメソッドです。また const_get は引数で指定した名前の定数の値を返します。

もし、あるモジュールやクラスでこのモジュールをインクルードすれば、そこではこれらの定数を名前演算子「::」を使うことなしにアクセスできます。現実的な例ではプロトコルを実装する場合があります。プロトコルのライブラリではコマンド名やデフォルト値など多くの定数が必要になりますが、これらの定数をアプリケーションで利用することがある場合はちょうど File::Constants のように定数だけをモジュールにまとめておくと便利でしょう。

Mix-inとしてのモジュール

Mix-inは多重継承の欠如を補います。まず、あるモジュールを複数のモジュールやクラスにインクルードできます。組み込みにあるモジュールでは EnumerableComparable が複数のクラスにインクルードされています。また逆に1つのクラスに複数のモジュールをインクルードできます。組み込みのモジュールでは StringEnumerableComparable をインクルードしています。

後者の用法は原理的には多重継承と同等の機能を与えるのですが、前者の用法を目指したモジュールの設計の方は汎用性を考える機会となり楽しいものです。たとえば、総和や平均などの統計処理を行ないたいことがあります。そこで統計処理を行なうための mix-in として Math::Statistics を作ってみましょう。統計が行なえるために必要な条件は、

  1. 要素を列挙できること
  2. 要素数が分かること
  3. 要素の四則演算ができること

です。1番目の条件からStatiesticsに必要な機能としてeachがあることが期待されます。2番目の条件は size を持っていることを期待します。この2つの要求はEnumerableであれば満たされます。そして最後の条件からは要素の四則演算ができることが期待されますが、例えばハッシュの値に関する統計をとりたい場合などには要素そのものの四則演算をしたいわけではありません。そこで、sumなどのメソッドにはその要素を取り出す方法が指定できると良いでしょう。

そこで次のようなメソッドを用意することにします。

sum
sum{ ... }

総和を返す。ブロックが与えられた場合は要素についてそのブロックを評価した値の総和を返す。以下、ブロックの扱いはこれと同様。

average
avg

平均を返す。

variance
var

分散を返す。

standard_deviation
std

標準偏差を返す。

Min

最小値を返す。

Max

最大値を返す。

MinMaxが大文字で始まっているのは、Enumerableminmaxとカチ合わないようにするためです。List1に実装例を示します。

-- List1 math/statistics.rb: 統計モジュール

  =begin

  = Math::Statistics

  == Usage

   ----
     require "math/statistics.rb"

     class Array
       include Math::Statistics
     end

     a = [1,2,3]
     puts a.sum, a.standard_deviation
   ----

  produces

   ----
     6.0
     0.8164965809
   ----
  =end

  module Math
    module Statistics

      def self.append_features(mod)
        unless mod < Enumerable
          raise "unexpected destination "\
            "`#{mod}' (must be Enumerable)"
        end
        super
      end

      def sum
        sum = 0.0
        if block_given?
          each{|i| sum += yield(i)}
        else
          each{|i| sum += i}
        end
        sum
      end

      def average(&blk)
        sum(&blk)/size
      end

      def variance(&blk)
        sum2 = if block_given?
                 sum{|i| j=yield(i); j*j}
               else
                 sum{|i| i**2}
               end
        sum2/size - average(&blk)**2
      end

      def standard_deviation(&blk)
        Math::sqrt(variance(&blk))
      end

      def Min(&blk)
        if block_given?
          if min = find{|i| i}
            min = yield(min)
            each{|i|
              j = yield(i)
              min = j if min > j
            }
            min
          end
        else
          min()
        end
      end

      def Max(&blk)
        if block_given?
          if max = find{|i| i}
            max = yield(max)
            each{|i|
              j = yield(i)
              max = j if max < j
            }
            max
          end
        else
          max()
        end
      end

      alias avg average
      alias std standard_deviation
      alias var variance
    end
  end

  if __FILE__ =~ $0
    class Array
      include Math::Statistics
    end

    a = [1,2,3]
    puts a.sum, a.standard_deviation
  end

この定義で現れる append_featuresinclude の実体です。すなわち、include Math::StatisticsArrayで評価されるとき、 includeMath::Statistics.append_features(Array) を呼び出します。ここで、仮引数 mod には include を呼び出したクラスが代入されるので、「mod < Enumerable」を使って modEnumerable であることを調べることでこのモジュールをインクルードするのにふさわしいかどうかを判断しています。たとえば、このモジュールを Object にインクルードするとFig1のようになります。

-- Fig1 List1の不適切な利用

   % cat stat-err.rb 
   require "math/statistics.rb"

   class Object
     include Math::Statistics
   end


   % ruby stat-err.rb 
   ./math/stattistics.rb:7:in `append_features': unexpected destination `Object' (must be Enumerable) (RuntimeError)
           from stat-err.rb:4:in `include'
           from stat-err.rb:4
   %

また append_features 本来の動作を行なうために、super を呼び出す必要があります。

このモジュールは Hash などにインクルードしてもブロックを与えれば統計処理を行なえますが、実際にはクラス毎にデフォルトのブロックを与える機構(たとえば default_block=)を用意したほうが使いやすいでしょう。これは宿題とします*3

ところで、このモジュールのように、XXX::YYY というモジュールのXXXの部分が一般的な範疇の場合、require "xxx/yyy" という具合のパスにマップされるという慣習があります。ライブラリの数が多いPerlなどではこれによって物件が探しやすくなっていますが、Rubyでもそろそろこういった階層化をみんなが日常的に検討しても良い時期に来ている気がします。現状の標準ライブラリでは NetIRBCGI がこの階層化を行なっています。

それと余談になりますが sum の定義の冒頭で sum = 0.0 として和の初期値を設定しています。もしこのモジュールをベクトルにも対応させるなら、初期値には抽象的な零を表す値を与える必要があるでしょう。初期値を引数で渡すのも一法ですが、筆者の個人的な見解では、「+」メソッドを持つクラスは zero というクラスメソッド、もしくは定数を持っていた方が良さそうです。この意見は以前 inject のからみで ruby-talk でも出たことがあります[ruby-talk:8797]

特異メソッドとモジュール

Rubyではオブジェクトごとにメソッドを定義できます。このようなメソッドは特異メソッドと呼ばれます。例えば、配列 ary にだけ sum というメソッドを追加して、要素の総和を返すようにするには、List2のようにすれば良いのです。

-- List2 sum0.rb: 特異メソッド定義の例

  ary = [10,-2,31,-8]

  def ary.sum(zero = 0)
    sum = zero
    each{|i| sum += i}
    sum
  end

  puts ary.sum  #=> 31

あまり使われているのを見かけませんが Object#extend を使うことで特異メソッドを複数のオブジェクトに追加することもできます。extendself、すなわちレシーバーを返します。extendinclude に似ていますが、クラスではなくオブジェクトに機能を付加する点が違います。 extend の使いどころは異なるクラスの特定のオブジェクトにメソッドを追加したい場合です。List3では ArrayStruct::Tmssum を追加しています。

-- List3 sum1.rb: extendを使って特異メソッドを定義

  module Sum
    def sum(zero = 0)
      sum = zero
      each{|i| sum += i}
      sum
    end
  end

  array = [10,-2,31,-8].extend Sum
  times = Time.times.extend Sum

  puts array.sum
  puts times.sum

なお、include の実体が append_features であるように、 extend の実体は extend_object となっています。

余談: 特異メソッドと特異クラス

Rubyプログラミングにおいてはちっとも重要ではないのですが、特異メソッドの実現は特異クラスを用いて行なわれます。Rubyのオブジェクトはそのオブジェクトだけをインスタンスとして持つメタなクラスに属しています。このメタなクラスを特異クラスといいます。Rubyの構文においては「class <<」で始まる特異クラス定義における self としてのみ、その特別なクラスにアクセスすることができます。List4の実験では特異クラス定義における self がそのオブジェクトの通常の意味でのクラスと異なることを示しています。

-- List4 singletonclass.rb: 特異クラスへのアクセス

  class Object
    def singleton_class
      class << self # この self はレシーバ
        self        # この self は特異クラス
      end
    end
  end

  Str = "a".singleton_class
  p [Str, String, Str.eql?(String)]
    #=> [String, String, false]

特異メソッドはこの特異クラスへのメソッドの追加にほかなりません。この関係はRubyプログラミングでは知らなくても全然構いませんが、特別なオブジェクトを定数として持つような拡張ライブラリを作る場合は自分が何をやっているのか分かるので知っておいたほうが良いでしょう。Rubyが提供するCのAPIである rb_define_singleton_method() は、まさにメソッドを特異クラスに追加するものです。その定義は class.c にあります。

ちなみにRubyのソースコードの一つ object.c には、この関係を表す大変興味深い図が含まれています(List5)。

-- List5 オブジェクトと特異クラスの関係(object.cより抜粋):
         カッコつきのクラス名は特異クラス、横向きの矢印は
         is_a? 関係、上向きの矢印は継承関係。

  Copyright (C) 1993-2000 Yukihiro Matsumoto
  Copyright (C) 2000  Network Applied Communication Laboratory, Inc.
  Copyright (C) 2000  Information-technology Promotion Agency, Japan

  /*
   * Ruby's Class Hierarchy Chart
   *
   *                           +------------------+
   *                           |                  |
   *             Object---->(Object)              |
   *              ^  ^        ^  ^                |
   *              |  |        |  |                |
   *              |  |  +-----+  +---------+      |
   *              |  |  |                  |      |
   *              |  +-----------+         |      |
   *              |     |        |         |      |
   *       +------+     |     Module--->(Module)  |
   *       |            |        ^         ^      |
   *  OtherClass-->(OtherClass)  |         |      |
   *                             |         |      |
   *                           Class---->(Class)  |
   *                             ^                |
   *                             |                |
   *                             +----------------+
   *
   *   + All metaclasses are instances of the class `Class'.
   */

mix-in でクラスメソッドを追加

mix-in のそもそもの用法からは外れるかも知れませんが、include のディスパッチである append_features を使えばクラスメソッドも include で追加することができます。標準ライブラリでは singleton.rb などがこれを利用しています。 とくに singleton.rb は短いのでその定義を List6 に抜粋しました。説明のために行番号をつけています。

-- List6 singleton.rb の抜粋

  01: module Singleton
  02:   def Singleton.append_features(klass)
  03:     klass.private_class_method(:new)
  04:     klass.instance_eval %{
  05:       @__instance__ = nil
  06:       def instance
  07:         Thread.critical = true
  08:         unless @__instance__
  09:           begin
  10:             @__instance__ = new
  11:            ensure
  12:              Thread.critical = false
  13:            end
  14:           end
  15:         return @__instance__
  16:       end
  17:     }
  18:   end
  19: end

singleton.rb はインスタンスが高々1つしかないことを保証するクラスを作るための mix-in です。このモジュールがインクルードされたクラスはクラスメソッド new を使うことができません。その代わりに常にユニークなインスタンスを返すクラスメソッド instance を使ってそのインスタンスを得ることができます。その実現方法を逐一みていきましょう。

List6の2行目からがその本体となる append_features の定義です。仮引数 klass には include を行なったクラスが渡されます。

3行目では new をプライベートなクラスメソッドにしています。ここで使われている private_class_methodClass のメソッドです。

4行目から17行目までが instance_eval です。その引数は文字列として渡されています。Object#instance_eval は文字列引数もしくはブロックとして渡されたコードの self をレシーバーに変えて評価するものです。この instance_eval のレシーバーは klass ですから、5行目から16行目までのあいだでの selfklass となります。なお、ここで %{ … } という形の文字列リテラルが使われていますが、このリテラルを使うと Emacs の ruby-mode でインデントが崩れないというメリットがあります。

5行目で @__instance__ といういかにも名前の衝突を避けたインスタンス変数に、nil を代入しています。このインスタンス変数をもつオブジェクトは、この文脈の self すなわちインクルードを呼び出したクラスである klass となります。

6行目から16行目まではこのクラス klass のクラスメソッド instance の定義です。まず7行目で Thread.criticaltrue にしてスレッドの切り替わりを止めています。これは、このクラスの唯一のインスタンスを生成し @__instance__ に代入するためで、もし Thread.critical を設定していなければ、複数のスレッドでこの部分が実行される可能性があり、それによってインスタンスの一意性が保証できなくなります。スレッドセーフという観点はなかなか思いつきませんが、 Rubyのスレッド機構を使う可能性がある場合は対処する必要があります。Ruby でスレッドセーフにするのはたいして面倒でもないので日頃から気をつけたいものです。

8行目で @__instance__ にインスタンスがまだ登録されていないか調べます。もし登録されていなければ10行目で new を使ってインスタンスを生成し、@__instance__ に代入しています。9行目から begin がはじまっていますがこれは確実に Thread.critical を解除するために11行目で ensure を使ういたいからです。

そして15行目で @__instance__ を返してクラスメソッドの定義が終ります。

参考までに、もしスレッドセーフにしていなければどういうマズいことがあるかをFig2に示します。ここでいう「スレッドセーフでない」とはList6で7行目と12行目をなくした場合です。

-- Fig2 Listがスレッドセーフでない場合に起こりうるシナリオ: 
        検査と登録の間で切り替わりが起こると、本来1度しか呼ばれる
        はずのない生成(new)が2度呼び出される。これだけでも悪いが、
        さらにもし次の切り替わりがreturnが終るまで起こらなければ、
        各スレッドはそれぞれが作ったインスタンスを返す。これは一意
        なインスタンスを返すという仕様に反する。


      スレッド1   |  スレッド2
    ============================
      検査        |
                >>>>>             スレッドの切り替わり
                  |  検査
                  |  生成、登録
                  |  return
                <<<<<             スレッドの切り替わり
      生成、登録  |
      return      |


    ただし、検査等はList6の次の位置

         8:  unless @__instance__      # 検査
        10:      @__instance__ = new   # 生成、登録
        15:  return @__instance__      # return

ふと思ったのですが、Singletonのインスタンス生成はincludeのときに行なった方が排他制御の機会が少ないので効率が良いかも知れません。

モジュールとパターン

ところで、singletonという言葉はデザインパターンの用語で、「あるクラスに対してインスタンスが1つしかないことを保証し、それをアクセスするためのグローバルな方法を提供する」パターンです。パターンというのは、オブジェクト発見のためのカタログです。オブジェクト指向プログラミングにおいて最も難しい問題であるクラスの設計、言い替えれば、何がオブジェクトかという問題に対し、すでに有用であることが知られた多くのクラス構成をデザインパターンとしてGammaらがまとめました[GHJV]。ある程度大規模なソフトウェアを開発する場合は知っておくべき話題でしょう。

蛇足ですがパターンという用語は建築で使われていたものに由来します。文献としてC.アレグザンダーの『パタン・ランゲージ』[Ale84]を挙げておきます。専門家の間ではアレグザンダーの考え方に対する批判も多くあるようですが、この本をめくると、有機的な結合を目指した建築パターンが数多く載っていて素人にはなかなか楽しいものです。建築が塀のような小さなものから都市のような大きなものまで扱い、さまざまなレベルに複雑さがあるという点では案外プログラミングに似ているのかも知れません。もし一般的な設計論というものが存在しうるならばソフトウェア設計が建築設計に学ぶところも少なくないはずです。

さて、プログラミングにおけるパターンはライブラリとしては再利用できないような抽象レベルをまとめたものですが、C++やJAVAではライブラリ化が困難なパターンもRubyではできちゃう場合があります。Singletonの他に標準で用意されているものには、 observer.rb があります。その用法はRuby本に詳しく解説されているので、ここで繰り返すことは避けましょう。それにしても、高次の抽象と思われているパターンが動的な言語であるRubyにおいてはライブラリになり下がるのはなかなか面白いことです。

Classというクラス

Class は Module のサブクラスです。そのクラスメソッドnewは新しい無名のクラスを生成して返します。Class.newの引数にクラスが与えられた場合はそのクラスのサブクラスを生成して返します。クラスの名前は最初に付けられた名前になります。Rubyの代入が名前をつけることだということを思い出しましょう。ただしクラスの名前は大文字で始まる名前でなくてはなりません。つまり最初に定数に代入したときにその定数がクラスの名前になります。例で見てみましょう。

  1. p a = Class.new
  2. p A = Class.new

上の1は#<Class 0lx81086e0>のような文字列を表示しますが、2は Aと表示します。Module.newも同様の動作をします。クラスにはモジュールと同様に名前空間としての機能もありますから。その名前は重要です。そこで、ModuleClassにはnameというメソッドがあります。

また、 Module#module_eval やその別名である class_eval を使えば生成したクラスの文脈でメソッドを定義することもできます。こういった機能があるにはありますが普段から使うものではないでしょう。やはり必要に応じて使っていきたいところです。もしコーディングの手間を省く目的で使うなら分かりづらさが増大することを覚悟する必要があります。

動的なクラス生成

では必要なときはどんな時でしょう? 考えられるのはクラスを動的に生成しなければならないときです。例としてパラメトライズされたクラスというものを考えてみます。

たとえば、Rubyの配列はどんなものでも格納できます。これはこれで便利ですが、場合によっては要素を整数に限りたいこともあるでしょう。配列の要素が格納されるのは配列を生成するときと追加するときですから、そこでチェックを設ければ良さそうです。また配列の要素を文字列に限りたいこともあるかも知れません。そこでパラメトライズされたクラスを考えるわけです。 ArrayOf(Integer) という関数が Integer の配列を表すクラスならば読みやすいし、使いやすいでしょう。そしてこの ArrayOf() はクラスを返すので、動的にクラスを生成する必要があるわけです。List7に実装例を示します。ただしすべての追加メソッドに対応しているわけではありません。

-- List7 array_of.rb: 要素の型が固定された配列クラス

  def ArrayOf(type)
    ArrayOf.get_class(type)
  end

  class ArrayOf < Array
    CLASS_POOL = {}
    ELEMENT_TYPE = Object

    class << self
      def [](*arg)
        new.replace(arg)
      end

      def get_class(type)
        Thread.critical = true
        begin
          CLASS_POOL[type] ||= Class.new(self).set_element_type(type)
        ensure
          Thread.critical = false
        end
      end

      def set_element_type(klass)
        self.const_set("ELEMENT_TYPE", klass)
        self
      end

      def element_type
        defined?(self::ELEMENT_TYPE) && self::ELEMENT_TYPE
      end

      def new(size = 0, filling = nil)
        if filling
          type_check filling
        end
        super
      end
    end

    # Not all method is override; 
    # They should be redefine (e.g, +, |). 

    def []=(*arg)
      if (arg.size == 2 && arg[0].is_a?(Range)) || arg.size == 3
        type_check(*arg[2])
      else
        type_check(arg[2])
      end
      super
    end

    def push(*arg)
      type_check(*arg)
      super
    end

    def replace(arg)
      type_check(*arg)
      super
    end

    def fill(*arg)
      type_check(arg[0])
      super
    end

    def element_type
      type.element_type
    end

    private

    def type_check(*elm)
      if e = elm.find{|i| ! i.is_a? element_type}
        raise TypeError, 
          "unexpected type of element `#{e.inspect}'" +
          "(#{e.type} for #{element_type})"
      end
    end
  end

  if __FILE__ == $0
    p ArrayOf(Integer)
    p a = ArrayOf(Integer)[1,2,3]
    p ArrayOf(Integer).equal? ArrayOf(Integer)
    p a.push 1.0 #!=> TypeError
  end

このArrayOf()という関数は念のために X.eql? Y ならば ArrayOf(X).equal? ArrayOf(Y) を満たすようにしてあります。つまり、同じパラメータを持つクラスは常に同一のものです。その実装は定数のハッシュに登録するというものです。ただ定数ハッシュに登録するとGCされませんから、もしGCの対象にしたい場合はWeakRefを使う必要があります。その実現に関しては咳さんの Flyaway [Sek:Flyaway] というライブラリが参考になるでしょう。

ArrayOf() は名前をつけるべき?

さて、前節で作った ArrayOf() ですが、このクラスが返すクラスは無名です。クラスに名前がないと一番困るのは、クラス定義構文を適用することできないことです。次の文は構文エラーになります。

class ArrayOf(String); end

クラス定義構文のclassというキーワードの次には定数名になりうるものが来なければならないからです。では、ArrayOf() はクラスに名前をつけてから返すべきでしょうか? 一概にはいえませんが、筆者はつけるべきとは思いません。なぜならどの名前空間で名前をつけるかはこのライブラリのユーザに選ぶ権利があると思うからです。

また以前筆者の友人が、クラスの名前を引数として渡すようなクラス生成関数を書いていましたが、これは分かりにくいと思いました。たとえば、

ArrayOfString = ArrayOf(String)

は誰がみても ArrayOf(String)ArrayOfString という名前をつけていることだと分かりますが、仮に第2引数にクラス名を渡すようにして、

ArrayOfString(String, "ArrayOfString")

としても、これがStringを要素とする配列を定義して ArrayOfString という名前をつけているということは一見しても分かりません。

動的なプログラミングの落し穴のひとつはこの辺にあるでしょう。Rubyに備わった機能を使えばRubyの「自然な」書き方とは全然ちがう書き方ができます。しかしそうするとプログラムの読みやすさは低下します。慣習から逸脱しているからです。クラス定義やインクルードのような機能を変更するときはそれを明示的にユーザに行なってもらうべきです。そうすることで、プログラムの読みやすさを保持することができるのです。

別の例としてはインクルードが既存のクラスを変更することが挙げられます。 require "jcode" すると既存のクラスのメソッドが変更されるのでその利用には注意が必要です。jcodeはPerl由来で、これはこれでいまさら変更するのはかえって混乱を招きそうですから今のままで良いと思いますが、 require は新たなクラスと関数を定義するにとどめたほうが混乱が少なそうです。逆に require するだけで明示的な include を抜きにメソッドの再定義をするような使い方はやめた方が良いと思います。

本稿の例では、statistics.rb がそうです。require "statistics" しただけでは単にモジュールが定義されるだけでなにも起こりません。これは上のような信念に基づいてのことです。

また statistics.rb を書いたとき、引数で与えられたクラスにインクルードするStatisticsという関数を用意することを一瞬考えました:

Math::Statistics Array

一見これはカッコ良い気がしますが、実際のところ次のように書くこともそれほど退屈ではありません:

class Array
  include Math::Statistic
end

そして、これが大事な点ですが、このRuby本来の書き方の方がはるかに分かりやすのです。

まとめ

Rubyの動的プログラミングのサポートの多くはirbの開発とともに追加されたという経緯があります。そのおかげか現在ではRubyでRubyの処理系を書くことも可能になりつつあります[RAA:MetaRuby]。これらのモジュールの機能には「なければできない」たぐいのことも割とあるので、とくにレファレンスの ObjectModuleClass および組み込み関数の項目にははもう一度目を通してみましょう。多くのヒントが隠れています。

いっぽうで、一見短く書けてカッコ良さげにすることの代償として分かりにくさを持ち込むことは少なくありません。程度にもよりますが、クラスの構造やメソッドを変化させるような操作はできるだけネイティヴの書き方をユーザにしてもらう方が良いでしょう。ただし require によってメソッドを再定義するのではなく単に追加する場合は混乱は小さそうです。このような場合は追加される機能をドキュメントすることで注意を促せば十分でしょう。

冒頭の繰返しになりますが、読みやすさについて考慮すべき点は次の2点です。

Rubyはできるだけこれらの条件を満たすように作られているように思えます。たとえばユーザが自分でイテレータを作成できることで、ロジックの道筋の上で近くにあるものをコード上でも近くに書くことができます(追いかけやすさ)。また変数の接頭辞の文字種がそのスコープを表すことで、代入の影響の範囲を予感できます。前者をより深く身につけるには人のコードを読んで慣習を知るという精進を積むこと、後者は言語の基本的な用法を逸脱しないことが一つの目安になります。心がけておきたいことは、明日の自分は他人のようなものということです。きのう書いたコードが分からないなんてことになったら、ほかの人が読んでも分からないに違いありません。分かりやすいコードを書いて、愛せるプログラム、そして愛されるプログラムを目指したいものです。

謝辞

さて、9回に渡りつれづれなるままにお送りしてきましたこの連載は、筆者の都合により残念ながら今回を持ちましてしばらくお休みをいただきます。読んで下さったすべての人にお礼を申し上げます。御批判や御感想は引続きお待ちしておりますので編集までお寄せ下さいませ。

当連載は多くの人の協力を得てなりたっていました。とりわけRuby界がほこるメーリングリストアーカイブ「blade」を維持して下さっている原信一郎さんには深く感謝します。資料として数え切れないほど利用しています。もちろんそのアーカイブが有意義なのは、たくさんの話題提供や質問もしくは解答をしてくれた世界中のRubyistのおかげです。気がついていない人もおられるかも知れませんがメーリングリストでは質問も重要な資源なのです。そして、Ruby という楽しい言語の創造主には、執筆への励ましや筆者の数多くの勘違いに対する訂正などですっかりお世話になりました。まつもとゆきひろさん、どうもありがとうございます。

おっと、いい忘れましたがRubyに関する連載がこれでなくなるわけではありません。次回からは mod_ruby や VIM/Ruby など数多くの先駆的かつ実用的なソフトウェアでおなじみのあの人の連載がスタートします。どうぞお楽しみに。それでは、また機会があればお目にかかりたいと思います。

Happy hacking!

参考資料

[Ale84]

クリストファー・アレグザンダー, 『パタン・ランゲージ』, 平田翰那訳, 鹿島出版会 (1984)

[GHJV99]

Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides, 『オブジェクト指向における再利用のためのデザインパターン 改訂版』, 本位田真一, 吉田和樹訳, ソフトバンクパブリッシング (1999)

[Sek:Flyaway]

<URL:http://www2a.biglobe.ne.jp/~seki/ruby/flyaway-1.0b1.tar.gz>

[Wit53]

Ludwig Wittgenstein, 『哲学探求』, ウィトゲンシュタイン全集第8巻, 藤本隆志訳, 大修館書店 (1976)

[RAA:MetaRuby]

Mathieu Bouchard, ``MetaRuby'', <URL:http://www.ruby-lang.org/en/raa-list.rhtml?name=MetaRuby>

[RAA:Statistics]

Gotoken, `Statisitics', <URL:http://www.ruby-lang.org/en/raa-list.rhtml?name=Math%3A%3AStatistics>

[ruby-talk:8797]

Cristoph Rippel, <URL:http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/8797>


*1 Pythonは制御のブロックをインデントで表現することが文法で決められています
*2 言語の意味は存在せず利用だけがあるとするヴィトゲンシュタインの言語観 [Wit53]
*3 解答例は[RAA:Statistics]