A Day in the Life

2011-06-15

Ruby で Array を継承したクラスをうまくあつかう

Ruby で Array を継承/Mix-inしたクラスで、自分で定義した便利なメソッドを利用したい時ってありますよね。そんなとき普通に

class MyArray < Array
  def odd
    select {|f| f % 2 == 0 }
  end
end

と定義してうまくいく、と思いがちですが

ary = MyArray.new([1,2,3]).odd
p ary.class #=> MyArray であってほしいのにArray

となってしまいます。これは Ruby の実装で Array や Enumrator の配列を返す実装はその名の通り "Array" を返すため、自分が期待してる Array を継承してるクラスのインスタンスではなくなってしまっています。

継承がダメなら委譲、というわけで標準ライブラリの delegate.rb を使ってみようと考えます。delegate.rb のライブラリコードの一番下のサンプルに

class ExtArrayArray)
  def initialize()
    super([])
  end
end 

ary = ExtArray.new
p ary.class
ary.push 25
p ary

というコードが書かれているので、おお、これはうまくいきそう!と思うんですがこれが罠で、配列を返すメソッドはもちろん Array のインスタンスが返ってきてしまい目的のことができないです。

なので全部のpublic なメソッドを上書きして、戻り値が Array の場合だけ自分自身を返すような module を作って Mix-in して解決!!1

module ArrayToSelfConvert
  def self.included(klass)
    methods = ::Array.public_instance_methods(true) - ::Kernel.public_instance_methods(false)
    methods |= ["to_s","to_a","inspect","==","=~","==="]
    methods.each {|method|
      define_method(method) {|*args, &block|
        res = super(*args, &block)
        if res.class == Array && method != 'to_a'
          cloned = deep_clone ? Marshal.load(Marshal.dump(self)) : self.dup
          cloned.clear.concat(res)
        else
          res
        end
      }
    }
  end
  attr_accessor :deep_clone
end
class MyArray < Array
  include ArrayToSelfConvert
  def odd
    select {|f| f % 2 == 0 }
  end
end
ary = MyArray.new([1,2,3]).odd
p ary.class #=> MyArray

わーい、…というかもっと良い方法はないんだろうか…

Ruby で HTTP の内容をトレースする

外部ライブラリを使ってテストを書くときや、デバッグ時に今どんな http のリクエストが送られてるかを知りたいとき、webmock を使うと知ることができる。

webmock は本来 http の stub つくるライブラリなんだけど、 allow_net_connect! と after_request を利用すると、実際のリクエストは出しつつも、結果をトレースすることができる。

require 'rubygems'
require 'webmock'

WebMock.allow_net_connect!
WebMock.after_request do |request_signature, response|
  puts "= Request #{request_signature} was made ="
  puts response.status.join(' ')
  puts response.headers.map {|key, val| "#{key}: #{val}" }.join("\n")
  puts "\n" + response.body unless response.body.empty?
end

require 'open-uri'
open('http://example.com/').read