2004-05-07 00:45:41 +0900 (1708d); rev 3
いかにも Ruby 特有ぽいものを中心に小技をあつめました。 Cookbook なんかと違うのは、具体的な課題を解決する方法ではなく 言語上のパターンにしぼったこと。
初期化されているかどうかわからない インスタンス変数を初期化するときは ||= を使う。
@ivar ||= "" # @ivar が既に非 nil ならばそのまま、nil なら "" を代入
1.6.2 まではこう書くと初期化されていない場合に警告が出ていたのだが、 1.6.3 からは ||= を使う場合に限り警告が出なくなった。 Mix-in のメソッド中での初期化などに便利である。
例えば配列のハッシュを作るとき、ハッシュのキーになにが来るか わからないとしよう。すると、最初から全てのキーに対して 配列を入れておくことができないので、とあるキーが最初に 登場したときに配列をセットしないといけない。以下はそれを 一発でやるコード。
table = {}
(table[key] ||= []).push val
二行目の括弧の中がポイント。同様にハッシュの配列なら
list = []
(list[i] ||= {})[key] ||= val
とできる。二行目は、テーブルを作ると同時に val を返す という点も重要だ。また val が一回しか出ていないので、 変数のみならず、コストのかかる計算や副作用のある式も置ける ところが便利である。
1.8 からはブロック付きの Hash.new を使うこともできる。 引数に渡すのとブロックを使うのでは意味が違うことに注意。
table = Hash.new {|h,k| h[k] = [] }
table[key].push val
# 以下は間違い
table = Hash.new([])
table['a'].push 1
table['b'].push 2
p table['b'] # [1,2]
基本的には initialize でデータ構造を集中的に指定することができるぶん ブロック付き Hash.new のほうが好ましいはずである。 なのであるが、どうも好きになれない。 なぜ好きになれないかと言うとそれはたぶん、 デフォルト値の配列がどこから湧いてくるのか絵的にイメージできないからだと思う。
一般のクラスだったらデフォルト値生成用の Proc を持っていても 納得できるのだけど、どうも Hash だと Proc ブロックを持つほどの 「隙間」が絵として存在しないような感じがしてしまう。 なにしろ Hash はリテラルがあるから、 絵的にリテラルがそのまま脳内でも展開されているようなのだ。 でもって Hash リテラルを使う場合は Proc が入る余地はない。 従って納得できない、のである。
そういうわけで自分ではあまり Hash.new は使いたくないのだが、 そんなの気にならん、という人は心おきなく Hash.new { } を使ってもらえばよいと思う。
最も簡単なのは String#> を使うことである。 ただしどこかの桁が二桁以上になると通用しなくなる。
'1.2.6' > '0.9.0'
Ruby のマイナーバージョン/パッチレベルに限って言えば、 二桁にならないことは保証されている。 メジャーバージョンはわからない。
デフォルトの inspect はインスタンス変数の中身まで 再帰的に表示するので大きいオブジェクトを p すると かなり悲惨なことになる。 そういうときはとりあえずこうしておこう。
def inspect
"#<#{self.class}>"
end
ちなみに 1.8 からは pp が使えることもお忘れなく。
こんなの常識だが念のため書いておく。
def someiter(&block) @array.each(&block) end
File.open(fname) {|f| str = f.read }
print str
は先に str に代入しておかないとエラーになる。 いっそのことメソッドにしてしまおう。
def read_all(fname)
File.open(fname) {|f|
return f.read
}
end
print read_all(fname)
また File.open に限って言えば、 1.8 以降では File.read(fname) が使える。
必要なものだけ map する。
enum.select {|i| cond?(i) }.map {|i| edit(i) }
どうでもいいが俺は collect より map が好きだ。 find より detect が好きだ。 find_all より select が好きだ。 ちなみに collect detect select inject の -ect シリーズは Smalltalk から来ているらしい。 一方 zip, map は Haskell から来ているらしい。
配列をつなげた大きな配列をつくるとき、 (以下の #edit は配列を返すとして)
result = []
enum.each {|i| result.concat edit(i) }
result
と書くかわりに、map して flatten できる。
result = enum.map {|i| edit(i) }.flatten
さらに Ruby 1.8 以降で使える別解
result = enum.inject([]) {|list,i| list.concat edit(i) }
ちなみに Haskell だと同様の働きをする Data.List.concatMap という関数が存在する。
例えば
dosomething arg1 dosomething arg2 dosomething arg3
は
[arg1, arg2, arg3].each do |a| dosomething a end
と変換できる。
特に
dosomething 'this' dosomething 'is' dosomething 'a' dosomething 'pen'
は
%w(this is a pen).each do |s| dosomething s end
と変換できる。
例えば
case str when 'some' then .... when 'any' then .... else .... end
は
def ???
:
__send__ 'do_' + str
:
end
def do_some() .... end
def do_any() .... end
と変換できる。
さらにハッシュを使って変換を複雑にできる。
TABLE = {
'some' => 'some',
'alias_some' => 'some',
'any' => 'any',
'alias_any' => 'any'
}
:
__send__ 'do_' + TABLE[str]
:
def do_some() .... end
def do_any() .... end
複数クラスにまたがってひとつのインターフェイスを 実装したいのだが、その実装は外に見せたくない。
→ 実装用メソッドを private にして __send__ で呼ぶ。
class A
def some(b)
b.__send__(:any)
end
end
class B
private
def any
....
end
end
もっとも、ここまですることもないような気はする。 コメントに「内部用、呼ぶな!」と書いておけば十分じゃなかろうか。
class Module
def alias_class_method(new, old)
instance_eval "alias #{new} #{old}"
end
end
class C
alias_class_method :newobj, :new
end
そのつど
class << self alias newname oldname end
と書いても全然かまわないが、 三行必要なのがイマイチ (と言いつつ次の項で使う)。
かなり作ったような例ではあるが、 以下のコードを見てほしい。
class Header
# 注: ここのクラスはどれも Header を継承している
TABLE = {
'date' => DateHeader,
'to' => MultiAddressHeader,
'sender' => SingleAddressHeader,
'from' => FromHeader,
:
}
class << self
alias newobj new
end
def Header.new(name, field)
c = TABLE[name.downcase] or
raise ArgumentError, "unknown header: #{name}"
c.newobj(name, field)
end
end
new を alias して呼ぶ (呼ばなければならない) ところがポイント。 ここで super を使ったりすると無限ループになる。
def some_iter
yield arg
end
some_iter do |arg|
....
end
このようなイテレータは次のように書き換えられる。
def some_iter(obj, mid)
obj.__send__(mid)
end
class SomeClass
def m(*args)
...
end
end
some_iter(SomeClass.new, :m)
こうしてもよい。
def some_iter(method)
method.call
end
class SomeClass
def m(*args)
....
end
end
some_iter(SomeClass.new.method(:m))
instance_eval の最も役に立つ使いかた。
val = obj.instance_eval { @ivar }
これ以外の目的にはあまり instance_eval を使わないほうがよい。
metaclass = (class << obj; self end)
ただし、それまで obj に特異クラスがなくても これをやった時点で自動的に特異クラスを生成してしまうので注意。
def breaked?(proc)
check_break(&proc)
end
def check_break(&block)
brk = true
begin
yield
brk = false
ensure
return brk
end
end
pr1 = proc { break }
pr2 = proc { }
pr3 = proc { next }
p breaked?(pr1)
p breaked?(pr2)
p breaked?(pr3)
たしかに break は検出できるのだが、 ついでに return も検出してしまう。困った。
例えば 1.6 で Array#filter が collect! と改名され、 filter を使うと警告が出るようになった。 この警告を回避しつつ互換性を保つには、 以下のようにして必要なときだけ Array#collect! を定義し、 コードでは collect! だけを使うようにする。
unless [].respond_to?(:collect!)
class Array
alias collect! filter
end
end
respond_to? を使うところと、 unless の中に class 文が存在できるというところがポイント。 また、クラスに対してメソッドの存在をチェックするのなら method_defined? を使う。
unless Enumerable.method_defined?(:detect)
module Enumerable
alias detect find
end
end
クラス名が定数だってのは常識。 ということは定数を定義すればクラスに別名をつけられるのも常識。
ClassAlias = SomeClass
以下のように、エイリアスに対して class 文を適用することも可能。
class C
p self # C
end
ClassAlias = C
class ClassAlias
p self # C
end
これは例外クラスで起こりやすい。 defined? を使って定数をチェックする。
unless defined?(ParseError) class ParseError < StandardError; end end
いきなり class ... end と書いても、 スーパークラスが同じか省略されている限り問題はないのだが、 このほうがやりたいことがより明確になると思う。
例えば高速な C 版と移植性の高い Ruby 版を両方提供したいという場合、 次のような型にはめるとよい。
# somelib.rb require 'somelib_r.rb' # SomeClass_R を定義 begin require 'somelib_c.so' # SomeClass_C を定義 SomeClass = SomeClass_C rescue LoadError SomeClass = SomeClass_R end
つまり、常に両方のクラスをロードしておいてクラス名で分岐する。 エラーが起きたときにクラス名だけ見ればどちらの バージョンを使っているかわかるのでデバッグに便利だ。
ただし、拡張ライブラリ化をやりすぎると メンテナンス性が非常に悪くなるので注意。
Related Pages: RubyPages
system revision 1.162