«前の日記(2010-05-03 (月)) 最新 次の日記(2010-05-05 (水))»  

まちゅダイアリー


2010-05-04 (火)

tDiary の1.8と1.9の互換性を向上したい

2年前にtDiaryの1.8と1.9の混在を試した時に、Stringの仕様変更に対応して以下のようなコードを書いた。

class String
  def method_missing(name, *args, &block)
    each_line.__send__(name, *args, &block)
  end
end

これはruby 1.9でStringがEnumerableでなくなった (eachメソッドを持たなくなった) ことに対応するため。

# ruby1.8.7
irb(main):001:0> "aaa\nbbb".map
=> ["aaa\n", "bbb"]
# ruby1.9.1
irb(main):001:0> "aaa\nbbb".map
NoMethodError: undefined method `map' for "aaa\nbbb":String
        from (irb):1
        from /opt/local/bin/irb1.9:12:in `<main>'

既存コードに手を入れたくなかったので、String#mapなどのEnumerable系のメソッドが呼ばれた場合に、method_missingを使ってeach_line経由で呼び出すようにしてた。

いま見てもこれはひどいコードなので、もう少し互換性をあげようと思う。

method_missingで反応するメソッドを絞り込む

元のコードはすべての存在しないメソッドに対してeach_line経由に変換してる。 このままだと "string".mapp のようなtypoにも反応してしまってバグの温床になる。 そこで、Enumerable系のメソッドだけに反応するようにして、それ以外はsuperを呼び出してちゃんと例外を返すようにした。

class String
        def method_missing(name, *args, &block)
                if each_line.respond_to?(name)
                        each_line.__send__(name, *args, &block)
                else
                        super
                end
        end
end

1.8系でもString#linesメソッドを使うようにする

これまでのように

body.map { ... }

とするのではなく、

body.lines.map { ... }

と書くようにする。 2年前にリリースされたruby1.8.7ではString#linesが追加されているので、1.8.7と1.9.1だと互換性が保たれている。 もちろん、1.8.6以前でも動くように compatible.rb に String#lines を定義しておく。

unless "".respond_to?('lines')
        class String
                alias_method :lines, :to_a
        end
end

String#eachが呼び出されるとログに記録する

linesを使うようにするため、String#eachが呼ばれるたびにログに呼び出し元を記録するようにした。

class String
        def method_missing(name, *args, &block)
                if each_line.respond_to?(name)
                        require 'logger'
                        log_file = "deprecated.log"
                        @@tdiary_compatibile_logger ||= Logger.new(log_file)
                        @@tdiary_compatibile_logger.warn("called deprecated method: String##{name}")
                        @@tdiary_compatibile_logger.warn(caller.join("\n"))
                        each_line.__send__(name, *args, &block)
                else
                        super
                end
        end
end

これを埋め込んでおいて、ログに出力された箇所をlinesを使うように置き換えていけばOK(本当は、テストコードで検出できるのが一番いいんだけどね)。 …と思って、早速ここの日記に埋め込んでみたけど、まだ1つもログが出力されない。 もしかして、もうmethod_missingを使ったモンキーパッチは不要なのかも。

進展があったら、GitHubのブランチでちまちま作業していく予定。

Tags: tDiary
本日のツッコミ(全2件) [ツッコミを入れる]
nagachika (2010-05-04 (火) 22:18)

1.9 に String の Enumerable のメソッドを(each_line相当として)復活させるのでしたら、each だけ each_line の alias として定義して String で include Enumerable してもいけないでしょうか。他のメソッドは each を使って Enumerable が実装してくれますので。<br><br>class String<br> alias :each :each_line<br> include Enumerable<br>end<br><br>String が Enumerable を ancestors に含んでしまういますけど、それも含めて 1.8 と同じではあるので。

まちゅ (2010-05-05 (水) 02:37)

なるほど!確かにこの方がスマートです。<br>ancestors って何だろうと思ったら、スーパークラスとincludeしているモジュールを返すのですね。<br><br>ただ、nameをログに書き出したいので、当面はこのままにしておきます。