後藤謙太郎 <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はクラスですが、
ClassはModuleのサブクラスなので、この場合は名前空間としても使われています。そして名前空間としての 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は多重継承の欠如を補います。まず、あるモジュールを複数のモジュールやクラスにインクルードできます。組み込みにあるモジュールでは
Enumerable やComparable が複数のクラスにインクルードされています。また逆に1つのクラスに複数のモジュールをインクルードできます。組み込みのモジュールでは String が Enumerable と
Comparable をインクルードしています。
後者の用法は原理的には多重継承と同等の機能を与えるのですが、前者の用法を目指したモジュールの設計の方は汎用性を考える機会となり楽しいものです。たとえば、総和や平均などの統計処理を行ないたいことがあります。そこで統計処理を行なうための mix-in として Math::Statistics を作ってみましょう。統計が行なえるために必要な条件は、
です。1番目の条件からStatiesticsに必要な機能としてeachがあることが期待されます。2番目の条件は size を持っていることを期待します。この2つの要求はEnumerableであれば満たされます。そして最後の条件からは要素の四則演算ができることが期待されますが、例えばハッシュの値に関する統計をとりたい場合などには要素そのものの四則演算をしたいわけではありません。そこで、sumなどのメソッドにはその要素を取り出す方法が指定できると良いでしょう。
そこで次のようなメソッドを用意することにします。
sum
sum{ ... }
総和を返す。ブロックが与えられた場合は要素についてそのブロックを評価した値の総和を返す。以下、ブロックの扱いはこれと同様。
average
avg
平均を返す。
variance
var
分散を返す。
standard_deviation
std
標準偏差を返す。
Min
最小値を返す。
Max
最大値を返す。
MinやMaxが大文字で始まっているのは、Enumerableの
min、maxとカチ合わないようにするためです。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_features はinclude の実体です。すなわち、include Math::StatisticsがArrayで評価されるとき、
includeは Math::Statistics.append_features(Array) を呼び出します。ここで、仮引数 mod には include を呼び出したクラスが代入されるので、「mod < Enumerable」を使って mod が
Enumerable であることを調べることでこのモジュールをインクルードするのにふさわしいかどうかを判断しています。たとえば、このモジュールを
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でもそろそろこういった階層化をみんなが日常的に検討しても良い時期に来ている気がします。現状の標準ライブラリでは Net と IRB、 CGI がこの階層化を行なっています。
それと余談になりますが 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 を使うことで特異メソッドを複数のオブジェクトに追加することもできます。extend
は self、すなわちレシーバーを返します。extend は
include に似ていますが、クラスではなくオブジェクトに機能を付加する点が違います。 extend の使いどころは異なるクラスの特定のオブジェクトにメソッドを追加したい場合です。List3では Array と
Struct::Tms に sum を追加しています。
-- 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 のそもそもの用法からは外れるかも知れませんが、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_method は Class のメソッドです。
4行目から17行目までが instance_eval です。その引数は文字列として渡されています。Object#instance_eval は文字列引数もしくはブロックとして渡されたコードの self をレシーバーに変えて評価するものです。この instance_eval のレシーバーは klass ですから、5行目から16行目までのあいだでの self は klass となります。なお、ここで %{ … } という形の文字列リテラルが使われていますが、このリテラルを使うと Emacs の ruby-mode でインデントが崩れないというメリットがあります。
5行目で @__instance__ といういかにも名前の衝突を避けたインスタンス変数に、nil を代入しています。このインスタンス変数をもつオブジェクトは、この文脈の self すなわちインクルードを呼び出したクラスである klass となります。
6行目から16行目まではこのクラス klass のクラスメソッド
instance の定義です。まず7行目で Thread.critical を
true にしてスレッドの切り替わりを止めています。これは、このクラスの唯一のインスタンスを生成し @__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 は Module のサブクラスです。そのクラスメソッドnewは新しい無名のクラスを生成して返します。Class.newの引数にクラスが与えられた場合はそのクラスのサブクラスを生成して返します。クラスの名前は最初に付けられた名前になります。Rubyの代入が名前をつけることだということを思い出しましょう。ただしクラスの名前は大文字で始まる名前でなくてはなりません。つまり最初に定数に代入したときにその定数がクラスの名前になります。例で見てみましょう。
p a = Class.newp A = Class.new上の1は#<Class 0lx81086e0>のような文字列を表示しますが、2は
Aと表示します。Module.newも同様の動作をします。クラスにはモジュールと同様に名前空間としての機能もありますから。その名前は重要です。そこで、ModuleとClassには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]。これらのモジュールの機能には「なければできない」たぐいのことも割とあるので、とくにレファレンスの Object と Module、 Class および組み込み関数の項目にははもう一度目を通してみましょう。多くのヒントが隠れています。
いっぽうで、一見短く書けてカッコ良さげにすることの代償として分かりにくさを持ち込むことは少なくありません。程度にもよりますが、クラスの構造やメソッドを変化させるような操作はできるだけネイティヴの書き方をユーザにしてもらう方が良いでしょう。ただし require によってメソッドを再定義するのではなく単に追加する場合は混乱は小さそうです。このような場合は追加される機能をドキュメントすることで注意を促せば十分でしょう。
冒頭の繰返しになりますが、読みやすさについて考慮すべき点は次の2点です。
Rubyはできるだけこれらの条件を満たすように作られているように思えます。たとえばユーザが自分でイテレータを作成できることで、ロジックの道筋の上で近くにあるものをコード上でも近くに書くことができます(追いかけやすさ)。また変数の接頭辞の文字種がそのスコープを表すことで、代入の影響の範囲を予感できます。前者をより深く身につけるには人のコードを読んで慣習を知るという精進を積むこと、後者は言語の基本的な用法を逸脱しないことが一つの目安になります。心がけておきたいことは、明日の自分は他人のようなものということです。きのう書いたコードが分からないなんてことになったら、ほかの人が読んでも分からないに違いありません。分かりやすいコードを書いて、愛せるプログラム、そして愛されるプログラムを目指したいものです。
さて、9回に渡りつれづれなるままにお送りしてきましたこの連載は、筆者の都合により残念ながら今回を持ちましてしばらくお休みをいただきます。読んで下さったすべての人にお礼を申し上げます。御批判や御感想は引続きお待ちしておりますので編集までお寄せ下さいませ。
当連載は多くの人の協力を得てなりたっていました。とりわけRuby界がほこるメーリングリストアーカイブ「blade」を維持して下さっている原信一郎さんには深く感謝します。資料として数え切れないほど利用しています。もちろんそのアーカイブが有意義なのは、たくさんの話題提供や質問もしくは解答をしてくれた世界中のRubyistのおかげです。気がついていない人もおられるかも知れませんがメーリングリストでは質問も重要な資源なのです。そして、Ruby という楽しい言語の創造主には、執筆への励ましや筆者の数多くの勘違いに対する訂正などですっかりお世話になりました。まつもとゆきひろさん、どうもありがとうございます。
おっと、いい忘れましたがRubyに関する連載がこれでなくなるわけではありません。次回からは mod_ruby や VIM/Ruby など数多くの先駆的かつ実用的なソフトウェアでおなじみのあの人の連載がスタートします。どうぞお楽しみに。それでは、また機会があればお目にかかりたいと思います。
Happy hacking!
クリストファー・アレグザンダー, 『パタン・ランゲージ』, 平田翰那訳, 鹿島出版会 (1984)
Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides, 『オブジェクト指向における再利用のためのデザインパターン 改訂版』, 本位田真一, 吉田和樹訳, ソフトバンクパブリッシング (1999)
<URL:http://www2a.biglobe.ne.jp/~seki/ruby/flyaway-1.0b1.tar.gz>
Ludwig Wittgenstein, 『哲学探求』, ウィトゲンシュタイン全集第8巻, 藤本隆志訳, 大修館書店 (1976)
Mathieu Bouchard, ``MetaRuby'', <URL:http://www.ruby-lang.org/en/raa-list.rhtml?name=MetaRuby>
Gotoken, `Statisitics', <URL:http://www.ruby-lang.org/en/raa-list.rhtml?name=Math%3A%3AStatistics>
Cristoph Rippel, <URL:http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/8797>
*1 Pythonは制御のブロックをインデントで表現することが文法で決められています
*2 言語の意味は存在せず利用だけがあるとするヴィトゲンシュタインの言語観
[Wit53]
*3 解答例は[RAA:Statistics]