$Id: minipattern.rd,v 1.8 2003/07/29 15:55:24 aamine Exp $
いかにも 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.map {|i| cond(i) ? edit(i) : nil }.compact
別解
enum.select {|i| cond?(i) }.map {|i| edit(i) }
なんとなく後者のが格好よさげである。
配列をつなげた大きな配列をつくるとき、
(以下の #edit
は配列を返すとして)
result = [] enum.each {|i| result.concat edit(i) } result
と書くかわりに、map
して flatten
できる。
result = enum.map {|i| edit(i) }.flatten
さらに 1.8
以降で使える別解
result = enum.inject([]) {|a,i| a << i }
ちなみに Haskell
だと同様の働きをする
Data.List.concatMap
という関数が存在する。
dosomething arg1 dosomething arg2 dosomething arg3
は
[arg1, arg2, arg3].each {|a| dosomething a }
と変換できる。
特に
dosomething 'this' dosomething 'is' dosomething 'a_pen'
は
%w( this is a_pen ).each {|s| dosomething s }
と変換できる。
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 Header # 注: このクラスはどれも Header を継承している TABLE = { 'date' => DateHeader, 'to' => MultiAddressHeader, 'sender' => SingleAddressHeader, 'mime-version' => MimeVersionHeader, : } class << self alias newobj new end def Header.new( headername, body ) klass = TABLE[arg.downcase] or raise ArgumentError, 'unknown name' klass.newobj headername, body end end
new
を alias
して呼ぶところがポイント。
ここで super
を使ったりすると無限ループになる。
class Module def alias_class_method( new, old ) instance_eval "alias #{new.id2name} #{old.id2name}" end end
そのつど
class << self alias newname oldname end
と書いても全然かまわないが三行必要なのが嫌い。
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 ) checkbreak(&proc) end def checkbreak( &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
文が存在できる
というところがポイント。
クラス名が定数だってのは常識。 ということは定数を定義すればクラスに別名をつけられるのも常識。
ClassAlias = SomeClass
以下のように、エイリアスに対して class
文を適用することも可能。
class C p self # C end ClassAlias = C class ClassAlias p self # C end
例外クラスでおこりやすい。
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
つまり、常に両方のクラスをロードしておいてクラス名で分岐する。 エラーが起きたときにクラス名だけ見ればどちらの バージョンを使っているかわかるのでデバッグに便利だ。