JSHashその2

4月のRubyist九州にて小ネタとして出したもの。以前のJSHashのすこし改良版。id:authorNariさん、遅くなってごめんなさい。

class BlankSlate
  instance_methods.each { |m| undef_method m unless m =~ /^__/ }
end

class JSHash < BlankSlate

  def initialize(org)
    @hash = org.inject({}) {|memo,i|
      k,v = i
      # SymbolはJSに合わせてStringに変換
      memo[ (k.kind_of? Symbol) ? k.to_s : k ] = v
      memo
    }
  end
  
  def method_missing(sym, *args) 
    name = sym.to_s
    if name =~ /=$/
      name = name[0..-2]
      val = args[0]
      if val.respond_to?(:call) && name[/^[_a-z][^ ]*$/]
        # evalで登録するので、識別子になれないキーはメソッド定義しない
        eval %Q{
          def #{name}(*_args)
            @hash["#{name}"].call(self,*_args)
          end
        }
      end
      @hash[name] = val

      # 代入メソッドで帰る値はこの返り値ではなくて言語仕様で決まっている
      return val
    else
      return @hash[sym] || @hash[name]
    end
  end

  def inspect
    return @hash.inspect
  end

  def [](key)
    return @hash[key]
  end
  
  def []=(key, value)
    return @hash[key] = value
  end
  
  # 好みの問題
  def each
    @hash.each {|i|
      yield i[0]
    }
  end
  def size
    return @hash.size
  end
  
end

class Hash
  def to_jsh
    return JSHash.new(self)
  end
end

##################################################
# 使い方サンプル

alias function lambda

a = {
  :symbol1 => "Nishi-kiwa",
  "string1" => "Higashi-kiwa", 
  :symbol2 => 1234678,
  "string2 key" => "value!",
  8888 => 9999
}

b = a.to_jsh

# プロパティでアクセスできます
puts "#property access"
puts b.symbol1, b.string1, b.symbol2, b["string2 key"], b[8888]
puts "size = #{b.size}"

# 変更できます
puts "#overwrite"
b.symbol1 = "object!"
b.string1 = "Iwahana"
b["string2 key"] = 12345678
b[8888] = nil
puts b.symbol1, b.string1, b.symbol2, b["string2 key"], b[8888]
puts "size = #{b.size}"

# toString のようなもの
puts "#toString, inspection"
puts b.inspect

# eachで総当りのアクセスも可能
puts "#for in"
b.each {|i|
  puts "key:#{i}  value:#{b[i]}"
}

# function はちょっと苦しい
puts "#JS Object1"
b.aaa = "lambda"
b.hello = function {|this,i|
  puts "Hello #{i} #{this.aaa}"
}
b.hello("JavaScript")

# あるオブジェクトに所属しているメソッドを受け渡せる
puts "#JS Object2"
c = {}.to_jsh
c.aaa = "function"
c.hello = b["hello"] # b.hello はムリ
c.hello("Ruby")

変更点は

  1. JSHashのコンストラクタでSymbol→文字列の変換
  2. メソッドの定義をevalに
  3. 代入メソッドの返り値を言語仕様通りに

ぐらい。

前回はClassのオブジェクトで無理矢理定義させていたのだけども、やっぱりインスタンスごとに関数を定義した方が自然だろうということで、特異メソッドをeval以外で登録する方法を少し探してみた。でも結局、メソッド定義のたびにModule作ってincludeする方法しか思いつかなかったので、素直にevalで定義。どうせevalで定義できないメソッドは呼べないので、今回はとりあえずこれで納得。

似たようなHashでない入れ物の仕組みとしては、添付ライブラリの ostruct がある。あと、組込みクラスのStructも手軽な入れ物として使える。

一方でJSHashでやりたかったことは、JavaScriptのオブジェクトのような「入れ物とメソッドの緩い関係」。メソッドの実装を別のオブジェクトと共有できるというのは、3年くらい前に新しいブロック記法がruby-talkで話題になっていたときにもいろいろ出てきた。RubyではUnboundMethodを使えば無理矢理メソッドをクラスから引っ張り出すことが出来るのだけども、bindできるインスタンスに制限があったり、匿名関数と自由に往復できないなど、普段のプログラミングの道具として使うものでは無い。C#delegateもちょっとちがう。JavaScriptは柔軟にできる反面、効率が悪かったり慣れないとthisで指す先が誰なのか分からなくなるなど、困った点も多いのだけども、そういう緩い感じが心地よいと思うこのごろなのであります。