Rubyのhashメソッドをきちんと実装するとある1つの方法

前回のエントリの続き。こんなmoduleを作ってみました。includeするだけでまともなhashが使えるようになります。

module Hashable

  require "digest/sha1"

  def hash
    class_name = self.class.to_s
    hash_str = class_name
    hash_str.concat("={")
    if not defined?(@@hashable_hash_use_vars) then
      @@hashable_hash_use_vars = hash_use_instance_vars()
    end
    @@hashable_hash_use_vars.each_with_index do |var_name_symbol, i|
      hash_str.concat(",") if 0 < i
      hash_str.concat(var_name_symbol.to_s)
      hash_str.concat("=")
      hash_str.concat(self.instance_variable_get(var_name_symbol).to_s)
    end
    hash_str.concat("}")

    if not defined?(@@hashable_hash_xor_num) then
      digest = Digest::SHA1.digest(class_name)
      digest_first64bit = digest[0, 64/8]
      @@hashable_hash_xor_num = digest_first64bit.unpack("Q").first # extract unsigned 64bit Fixnum
    end

    return @@hashable_hash_xor_num ^ hash_str.hash
  end

  def eql?(obj)
    return false if !obj.kind_of?(self.class)
    obj_vars = {}
    obj.instance_eval do
      hash_use_instance_vars().each do |sym|
        obj_vars[sym] = self.instance_variable_get(sym)
      end
    end
    my_ivars = hash_use_instance_vars()

    return false if obj_vars.keys.sort != my_ivars.sort

    obj_vars.each do |sym, val|
      return false if val != self.instance_variable_get(sym)
    end

    return true
  end


  private

  def hash_use_instance_vars
    self.instance_variables
  end

end

コードはパフォーマンスを少しでもよくするため冗長になってます。簡潔にするとかわりに30~35%ぐらい遅くなります。Hashableで下のようなシンプルなモデルのhash値を計算した場合、だいたいString#hashと比べて10倍ぐらい遅いです。

基本的なアイディアとしては前回のエントリ(Rubyのhashメソッドをきちんと実装するには?)を踏襲していますが、排他的論理和に使うビットマスクをクラス名をSHA1したものを使って自動化しています。意外と再利用性がありそうだったのでモジュールにしました。

以下のようなパターンの場合、うまく動かないかもしれません。

  • インスタンス変数がまったくない, そもそも等値性関係ないのでこのモジュール使う意味ないですね。。。
  • インスタンス変数がinitialize以外のメソッドで生成される(=初めて代入される)

2点目について補足すると、基本的にリフレクションでインスタンス変数を全部取り出してきているので、等値性に関係ないインスタンス変数があとから増えちゃう場合はそれが考慮されない、という意味です。こういう場合の抜け道もいちおう簡単なものを用意してありまして、hash_use_instance_varsメソッドを以下のようにオーバーライドするとよいです。(別の要求として、あとからインスタンス変数が増えるのもhash値に織り込んで計算させたい場合は@@hashable_hash_use_varsクラス変数へのキャッシュを外してもいいかもしれません。)

# @a, @bに基づいて等値性を判断するのだけど@hogeが邪魔をしてしまうのを回避
class Hoge
  
  include Hashable

  attr_accessor :a
  attr_accessor :b
  
  def initialize(a, b)
    @a, @b = a, b
  end
  
  def hoge
    @hoge = "hoge"
    puts @hoge
  end
  
  private
  
  def hash_use_instance_vars
    [:@a, :@b]
  end
  
end

ちなみにこのHashableの実装はString#hashに依存しているのですが、String#hashは同じ文字列に対しても、同じrubyプロセス内では何回評価しても同じ値ですが、コマンドを複数回実行すると毎回違う値が出てきます。従って、まずありえないとは思いますがString#hashの出力や、Hashableのhashを使って求めたハッシュ値を永続化して、同一でないプロセス上で他のインスタンスハッシュ値と比較したりすると変なことになるはずです。


あわせて読みたい

追記:ちらばり具合に関する定量的評価を取ったのを忘れていました、以下折り畳んで追記

続きを読む

交通事故に遭った

車同士の右直事故で、自分は直進車でした。

  • 車が横転して裏返しになった, 中で死んでたら後味悪すぎると思ったけど大事には至らなくてよかった...
  • やっぱり事故の瞬間がスローモーションになるというのは本当だった
  • 現場に長時間拘束されて寒かった、何人もの警察官に状況を何度も何度も説明してすごく大変だった...
  • しばらく震えが止まらなかった

任意保険には入ってるし、基本的には相手の過失割合の方が圧倒的に高いのだけど、乗ってた車が非常に古いこともあって全損の可能性もあるのでもしかしたら車なしライフになってしまうかも... 不幸。

詳しいことはオフラインでお尋ねください。というかこの日記?ブログ?を呼んでる人でオフラインの知り合いがいるのかどうかは知らないけど...

なぜRubyを使うのか

最近なぜPerlを使うのか?という問いに対するアンサーエントリーがいろんなところで見られますが、これは各言語のユーザがやってみるとその言語のポジティブな部分が見れていいのではないかと思いますので僕もLLでは一番気に入っているRubyをなぜ気に入っているのか、を書きたいと思います。

文法

これは好みの問題なので、全ての人が他の言語よりRubyに魅力を感じる要因にはなりえないと思いますが、僕はRubyの文法が気に入っています。Perlはデフォルトでグローバル変数だったり、Pythonはインデントを閉じなかったり、というのが個人的にはあまり好きじゃなくて消去法的にRubyを使っているのかもしれません。

  • メソッドチェインによる日本語っぽい記述: タイプが楽
  • メソッド呼び出しでカッコを省略できる: タイプが楽
  • 柔軟性: 強力なメタプログラミングが可能(時には害にもなり得るとは思いますが)
  • 素直なローカル変数スコープ, 変数名先頭記号によるわかりやすいスコープ
  • ブロックつきメソッド呼び出し(メソッドにdo endをつけて呼び出す)
  • 純粋なオブジェクト指向言語であること

RubyGems

PerlCPANに対するRubyのソレは今まではRAA(Ruby Application Archive)でしたが、最近はgemコマンドで簡単にライブラリがインストールできるのがすごく便利なので、みんなこちらのRubyGemsを使っているイメージですね。

LinuxMacなど環境に関係なく動いてくれるところもうれしいです。優秀なパッケージマネージャーがあることでツールとしての魅力が高まっていると思います。

割とメジャーになってきた

最近では先進的なオープンソースプロダクトの言語別ライブラリでも割と早い段階でサポートされることも多くなってきましたし、Ruby on Railsなどの活躍でレンタルサーバでもrubyが標準的にインストールされていることが増えてきています。PerlPHPなどの普及度にはかなわないかもしれませんが、Rubyをメインのツールにしていても困らない状況になってきています。

まとまらず...

言うほどRubyの魅力をアピールできてない気がしますが、まあこんな感じでRubyを気に入って使ってます。

とろけるチキン問題

ネット上でなりすまされるのを防ぐにはどうすればいいんだろう。

有名人は認証済みアカウントとかあるっぽいけど、それほど有名じゃない個人や団体、組織・会社などでは自分のプレゼンスをそこまで高く評価してないから誰もなりすますなんて思わないよね、と思ってると思うのだけど、されてる可能性は十分あるわけで。

とろけるチキンに絡まれた: http://togetter.com/li/93253
とろけるチキン、実際に店舗に行ってみた: http://togetter.com/li/93481
とろけるチキン騒動第二幕 〜炎上マーケティング?なりすましの嫌がらせ?〜: http://togetter.com/li/93614

30byte FizzBuzz問題チャレンジ

i=0;loop{i+=1;m="";m<<"Fizz" if (0==i%3);m<<"Buzz" if (0==i%5);puts(m!=""?m:i)}

79が限界でした。(5分で挫折)

ちょっとがんばったら75になった。そして71になった。

# 75
i=0;loop{i+=1;m=""<<(i%3==0?"Fizz":"")<<(i%5==0?"Buzz":"");puts(m!=""?m:i)}

# 71
i=0;loop{i+=1;m=(i%3==0?"Fizz":"")<<(i%5==0?"Buzz":"");puts(m!=""?m:i)}

Perlだと70でできた。bare word卑怯だなー。

for(;;){$i++;$m=($i%3==0?Fizz:"").($i%5==0?Buzz:"");print $m||$i,"\n"}

しかし、30byteには達しそうもないので、僕はたぶんYahooではエンジニアとして働けないのでしょう。無念。

Rubyでバイナリデータをヘキサダンプするには?

バイナリデータをコンソールに出力して確認したいときとかなどにたまに使う。

bin = "\xa1\xb2\xc3"
p bin.unpack("H*").first #=> "a1b2c3"

firstのあとにupcaseメソッドをチェインすれば大文字になる("A1B2C3"のように)