RubyMiniPattern

2004-05-07 00:45:41 +0900 (1574d); 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

デフォルトの inspect はインスタンス変数の中身まで 再帰的に表示するので大きいオブジェクトを p すると かなり悲惨なことになる。 そういうときはとりあえずこうしておこう。

def inspect
  "#<#{self.class}>"
end

ちなみに 1.8 からは pp が使えることもお忘れなく。

ブロック丸投げ

こんなの常識だが念のため書いておく。

def someiter(&block)
  @array.each(&block)
end

DelegateRubyBlock

イテレータから変数を介さず結果を得る

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 + compact

必要なものだけ 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 から来ているらしい。

map + flatten

配列をつなげた大きな配列をつくるとき、 (以下の #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

と変換できる。

case をハッシュで置きかえ

さらにハッシュを使って変換を複雑にできる。

  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

もっとも、ここまですることもないような気はする。 コメントに「内部用、呼ぶな!」と書いておけば十分じゃなかろうか。

クラスメソッドを alias する

  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

と書いても全然かまわないが、 三行必要なのがイマイチ (と言いつつ次の項で使う)。

new の引数からクラスをダイナミックに決定する

かなり作ったような例ではあるが、 以下のコードを見てほしい。

  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 に特異クラスがなくても これをやった時点で自動的に特異クラスを生成してしまうので注意。

break を検出する

  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

クラスを alias

クラス名が定数だってのは常識。 ということは定数を定義すればクラスに別名をつけられるのも常識。

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 と書いても、 スーパークラスが同じか省略されている限り問題はないのだが、 このほうがやりたいことがより明確になると思う。

拡張モジュールと Ruby と両方で同じクラスを提供する

例えば高速な 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

つまり、常に両方のクラスをロードしておいてクラス名で分岐する。 エラーが起きたときにクラス名だけ見ればどちらの バージョンを使っているかわかるのでデバッグに便利だ。

ただし、拡張ライブラリ化をやりすぎると メンテナンス性が非常に悪くなるので注意。


system revision 1.162