Hatena::ブログ(Diary)

ザリガニが見ていた...。 このページをアンテナに追加 RSSフィード

2014-11-17

with_indifferent_accessはなぜ文字列キーを使うのか?

前回からの続き。

  • ところで、最近のRubyは{abc:123}({:abc => 123}と同等)のようなハッシュ定義も可能になり、間違いなくシンボルキーが多用される時代なのに...

with_indifferent_accessは、なぜ文字列キーを使うのか?


  • シンボルは無駄にオブジェクトを生成しない。
    • コード中に:fooがいくつあっても、たった一つの:fooオブジェクトを参照するだけ。
irb(main):001:0> :foo.object_id
=> 538568
irb(main):002:0> :foo.object_id
=> 538568
irb(main):003:0> :foo.object_id
=> 538568
    • オブジェクトIDがすべて538568。同じである。
  • 一方、文字列は何度も異なるオブジェクトを生成する。
    • コード中に"foo"が出現する度に、異なる"foo"オブジェクトを生成している。
irb(main):004:0> 'foo'.object_id
=> 70228655214520
irb(main):005:0> 'foo'.object_id
=> 70228655232480
irb(main):006:0> 'foo'.object_id
=> 70228655227360
    • オブジェクトIDの下6桁が、すべて違っている。
  • 以上のことからも、シンボルの方が無駄なことをせずに良さそうなのだが、なぜ文字列キーが使われるのか?
  • 実は、この無駄のないシンボルの性質が、逆に弱点となる場合もあったのだ。

  • シンボルはたった一つのオブジェクトしか生成しない。
  • そして、一度生成されたシンボルは、後の再利用のためにメモリ中にずっと残る。
  • つまり、GC(ガベージコレクション*1)の対象にならないのだ。
  • 一方、文字列はオブジェクトを繰り返し生成する。
  • しかし、使われなくなるとGCされ、メモリは解放されるのだ。

  • Webアプリケーションのような不特定な外部入力を受け取って、ハッシュに変換して処理する状況では、
  • GCされるかどうかの違いによって、シンボルは弱点になってしまうこともあるらしい。
  • GCされないということは、多分もう使われないシンボルであっても、rubyプロセスを再起動するまでずっと残り続けることになる。
  • シンボルには、無駄にオブジェクトは生成しないけれど、一旦生成されるとずっとメモリを占有し続ける、という性質があったのだ。
  • そうは言っても、今どきのサーバーはメモリも潤沢だし、たかがシンボルごときのメモリ占有によって、問題になるような影響は出ないんじゃないか?と思ってしまう。
  • 善意のユーザーが想定の範囲内で利用する場合においては、たぶんメモリ不足のような問題は発生しない。
  • ところが、悪意のあるユーザーが何らかの攻撃を仕掛けてきたら、目論見どおりメモリ不足になってしまうことがあるらしい。
  • そのような悪意のある攻撃に対抗するために、with_indifferent_accessは文字列キーを選択していたのだ。

  • indifferentなハッシュは、その内部は文字列キーに統一されている。
  • でもアクセスする時には、シンボル・文字列どちらのキーも使える。
irb(main):001:0> require 'active_support/all'
=> true

irb(main):002:0> h={abc:123}.with_indifferent_access
=> {"abc"=>123}

irb(main):003:0> h[:abc]
=> 123

irb(main):004:0> h[:efg]=456
=> 456

irb(main):005:0> h
=> {"abc"=>123, "efg"=>456}

参考ページ

素晴らしい情報に感謝です!

シンボルの脆弱性
シンボルの未来

*1:不要になったメモリ領域を解放する処理のこと。

2014-11-16

Rubyのハッシュにメソッドでアクセスするには?やらない方がいい?

JavaScriptな脳状態でRubyを触り始めると、ハッシュにアクセスする時にエラーで怒られる。

h={abc:123}
=> {:abc=>123}

h.abc
NoMethodError: undefined method `abc' for {:abc=>123}:Hash
	from (irb):15
	from /usr/bin/irb:12:in `<main>'

いかん、いかん、Rubyのハッシュはメソッド呼び出しではなく、[:abc]のように指定するのだ。

h[:abc]
=> 123

Rubyのハッシュは、キーとメソッドを明確に区別することで、キーの自由度はとっても広がった。

  • メソッド名と同じキーも使えるし、
  • シンボルや文字列に限らず、あらゆるオブジェクトがキーとなる。
irb(main):022:0> require 'date'
=> true

irb(main):023:0> date=Date.parse("2014-11-23")
=> #<Date: 2014-11-23 ((2456985j,0s,0n),+0s,2299161j)>

irb(main):024:0> h[date]="勤労感謝の日"
=> "勤労感謝の日"

irb(main):025:0> h
=> {:abc=>123, #<Date: 2014-11-23 ((2456985j,0s,0n),+0s,2299161j)>=>"勤労感謝の日"}

一方、オブジェクトは区別される。よって、シンボルと文字列も区別される。忘れると謎のnilに悩む...。

irb(main):027:0> h={'abc'=>123}
=> {"abc"=>123}

irb(main):028:0> h[:abc]
=> nil

irb(main):029:0> h['abc']
=> 123
  • シンボルキーと文字列キーがごちゃ混ぜのハッシュとならないように、事前に決断しておきたい。
  • ところが、せっかくシンボルキーに決断したのに、利用するライブラリは文字列キーを返したり、
  • 世の中、なかなか思いどおりにはいかない...。

いっそのこと、キーの自由度は狭まるけれど、場合によってはメソッドアクセスできた方が幸せを感じるかもしれない、と思い始めた。

メソッドアクセスの仕様

  • 良きに計らい、シンボルキーでも、文字列キーでも、メソッドアクセスできるようにしたい。
  • そうは言っても、h={'abc'=>123, :abc=>456}のようなハッシュも存在するので、どちらのキーを優先するのか決めておく必要もある。
    • 最近のハッシュ表現は、シンボルキーが多い。
irb(main):033:0> h={abc:123}
=> {:abc=>123}
    • よって、シンボルキーを優先することにした。
  • 読み出しだけでなく、書き込みもできるようにしておきたい。

コード

  • refineで拡張しているので、以下のコードを書いたファイル内でのみ、ハッシュへのメソッドアクセスが可能になる。
  • method_missingを利用しているので既存メソッドが優先される。
  • よって、メソッド名と同じキーには、メソッドアクセスできない。
    • 但し、従来の[キー]アクセスは可能。
module HashEx
  refine Hash do
    def method_missing(method, *args)
      if method[-1] == '='
        string = method[0..-2]
        symbol = string.to_sym
        key?(symbol) ? self[symbol] = args[0] : key?(string) ? self[string] = args[0] : self[symbol] = args[0]
      else
        key?(method) ? self[method] : self[method.to_s]
      end
    end
  end
end
using HashEx

実験

> h              # => {"abc"=>123, :abc=>456}
> h.abc          # => 456
> h.abc = 999    # => 999
> h              # => {"abc"=>123, :abc=>999}
> h.delete(:abc) # => 999
> h              # => {"abc"=>123}
> h.abc = 666    # => 666
> h              # => {"abc"=>666}
> h.xyz = 789    # => 789
> h              # => {"abc"=>666, :xyz=>789}
  • 読み出し
    • メソッド名と同じ値のシンボルキーを優先して読み出す。
    • シンボルキーが見つからない時は、文字列キーを読み出す。
  • 書き込み
    • メソッド名と同じ値のシンボルキーに優先して上書きする。
    • シンボルキーが見つからない時は、文字列キーに上書きする。
    • シンボルキーも文字列キーも見つからない時は、新たなシンボルキーに書き込みする。

活用した時のコード風景の違い

キーアクセス(従来)

f:id:zariganitosh:20141115080934p:image:w600

メソッドアクセス

f:id:zariganitosh:20141115080932p:image:w600


  • メソッドアクセスの方が、タイプ数が2文字分少ないので、若干コードが短くなる。
  • メソッドアクセスの方が、.をタイプするだけなので、入力時の負担が少ない。
    • 但し、最近のコードエディタを使えば補完機能が優秀なので、ほとんど差はないかもしれない。
  • 逆にメソッドアクセスにしてしまうと、他人がコードを見た時に、ハッシュ内の値を取り出したいのか、メソッドで操作したいのか、分かりにくくなる。
  • シンボルに対しては特定の強調されたシンタックスハイライトを設定可能なので、コードを読む時は従来のキーアクセスの方が読みやすい気がした。

もう一つのメソッドアクセス

  • ハッシュへのメソッドアクセスのことを調べていると、OpenStructという標準ライブラリがあることに気付いた。
  • OpenStructはハッシュではないのだけど、メソッドアクセス可能なキーと値の要素を内部に保持するオブジェクト。
irb(main):085:0> require 'ostruct'
=> true

irb(main):068:0> os=OpenStruct.new
=> #<OpenStruct>

irb(main):069:0> os.abc
=> nil

irb(main):070:0> os.abc=123
=> 123

irb(main):071:0> os.abc
=> 123

irb(main):072:0> os.abc=999
=> 999

irb(main):073:0> os.abc
=> 999

irb(main):074:0> os
=> #<OpenStruct abc=999>

irb(main):075:0> os.delete_field(:abc) # 引数'abc'を指定してもOK
=> 999

irb(main):076:0> os.abc
=> nil

irb(main):077:0> os
=> #<OpenStruct>

  • ハッシュからOpenStructを生成することもできる。
irb(main):081:0> h={abc:123, efg:456}
=> {:abc=>123, :efg=>456}

irb(main):082:0> os=OpenStruct.new(h)
=> #<OpenStruct abc=123, efg=456>

  • シンボルキーと文字列キーの文字表現が同じ場合、最後に評価されたキーが優先されるようだ。
    • 但し、ハッシュ内部でキーの順序がどのように評価されるか不明。(自分は知らない)
    • よって、ハッシュを利用しているうちに、予想外に順序が変化することもあるかも?
irb(main):083:0> os=OpenStruct.new({:abc=>111, 'abc'=>999})
=> #<OpenStruct abc=999>

irb(main):084:0> os=OpenStruct.new({'abc'=>999, :abc=>111})
=> #<OpenStruct abc=111>

      • eachは使えないけど、each_pair(Ruby2.0以降)が使える。
irb(main):001:0> require 'ostruct'
=> true

irb(main):002:0> os=OpenStruct.new({abc:123, efg:456})
=> #<OpenStruct abc=123, efg=456>

irb(main):003:0> os.each {|i| p i}
=> nil

irb(main):004:0> os.each_pair {|i| p i}
[:abc, 123]
[:efg, 456]
=> {:abc=>123, :efg=>456}

      • to_h(Ruby2.0以降)するとシンボルキーのハッシュを生成する。
      • よって、必要に応じてto_hすれば、ハッシュと同等に使えるのだ。
      • eachだって、こうすれば使える。
irb(main):013:0> os.to_h.each {|i| p i}
[:abc, 123]
[:efg, 456]
=> {:abc=>123, :efg=>456}

Railsのハッシュ拡張

irb(main):001:0> require 'active_support/all'
=> true

  • requireだけでは、ハッシュは従来どおりの機能である。
  • シンボルキーと文字列キーは、明確に区別される。
irb(main):002:0> h={abc:123, efg:456}
=> {:abc=>123, :efg=>456}

irb(main):003:0> h
=> {:abc=>123, :efg=>456}

irb(main):004:0> h[:abc]
=> 123

irb(main):005:0> h['abc']
=> nil

  • 一方、with_indifferent_accessというハッシュの拡張があって、(indifferent=無頓着な、どっちでもかまわない)
  • with_indifferent_accessで生成したハッシュは、シンボル・文字列どちらのキーでもアクセス可能になるのだ!
irb(main):006:0> h=h.with_indifferent_access
=> {"abc"=>123, "efg"=>456}

irb(main):007:0> h
=> {"abc"=>123, "efg"=>456}

irb(main):008:0> h['abc']
=> 123

irb(main):009:0> h[:abc]
=> 123
  • with_indifferent_accessは、元のハッシュから、すべて文字列キーのハッシュを生成する。
  • そして、生成されたindifferentな文字列キーのハッシュは、シンボルキーでもアクセス可能になるのだ!
    • with_indifferent_access!という破壊的メソッドはないので、同じ変数に代入している。
  • いや、そもそも途中でハッシュの性質を変えるのはナンセンス。本来は、最初の定義でwith_indifferent_accessを指定しておくべきなのだと思う。
irb(main):015:0> h={abc:123, efg:456}.with_indifferent_access
=> {"abc"=>123, "efg"=>456}

素晴らしい!


  • 結局、自分が求めていたのはメソッドアクセス可能なハッシュではなくて、indifferentなハッシュなのだと思う。
  • シンボルキーと文字列キーが混在してしまう時の苦労をしたくない、という欲求が脳内の根底にあった。
  • JavaScript脳の状態でRubyを使いだすと、メソッドアクセスできないことに不便を感じたが、それは自分自身がRubyモードになるだけで解決できる。
  • 後のコードの読みやすさを考えれば、キーによるハッシュ要素へのアクセスなのか、メソッドによるハッシュ操作なのか、区別できた方が読み易いはず。
  • たった2文字の節約のために、Rubyらしいコード表現を隠してしまうのは、改善ではなく、改悪に向かっていた。

教訓:基本クラスのオレオレ拡張より、先達の知恵。

2014-11-06

bundlerでgemバージョンを束ねる

Rails1.1.6の環境でどうにかscaffoldはできるようになった。その時の実行環境は以下の状態だった。

$ rbenv version
1.8.7-p375 (set by /Users/zari/.rbenv/version)

$ gem list

 *** LOCAL GEMS ***

actionmailer (1.2.5)
actionpack (1.12.5)
actionwebservice (1.1.6)
activerecord (1.14.4)
activesupport (1.3.1)
rails (1.1.6)
rake (0.7.1)
sqlite3 (1.3.10)
  • この実行環境というのは非常にデリケートである。
  • 例えば、今の時代にRails-1.1.6をgemに従うままにインストールすると、最新のRake-10.3.2がインストールされる。
  • しかし、Rake-10.3.2ではRails-1.1.6はうまく動作しない。
    • rake db:migrateでエラーが出てしまう...。
  • このエラーに散々悩んで、ようやくrakeのバージョンを下げれば良いことに気付いた。
  • そこで、わざわざgem uninstall rakeで一旦rakeを削除して、その後、Rake-0.7.1を再インストールしている。面倒くさい。
  • しかも、動かしたい古いRails環境のバージョンは1.1.6だけではない。
  • バージョン1.2.6や2.0.5などのRails環境も動かしたいのだ。
  • それらのバージョンをインストールした瞬間に、Rails-1.1.6のプロジェクトは正常に動かなくなってしまう。
  • gemコマンドの操作は手軽さが売りなのに、これでは気楽にインストールできない。
    • gem updateなんて、もってのほかである。
  • プロジェクトはgemのバージョンと密接な関係があるはずなのに、インストールされた最新バージョンしか使えない所に問題がある。
  • 必要なgemとバージョンを自由に選択して、実行環境を素早く作り上げる仕組みが必要なのだ。

それを実現してくれるのがbundlerなのだった。

基本

bundlerをインストール
  • bundlerもまたgemなので、gemコマンドでインストールするのだ。
$ gem install bundle
Fetching: bundler-1.7.4.gem (100%)
Fetching: bundle-0.0.1.gem (100%)
Successfully installed bundler-1.7.4
Successfully installed bundle-0.0.1
2 gems installed
Installing ri documentation for bundler-1.7.4...
Installing ri documentation for bundle-0.0.1...
File not found: lib
ERROR:  While generating documentation for bundle-0.0.1
... MESSAGE:   exit
... RDOC args: --ri --op /Users/zari/.rbenv/versions/1.8.7-p375/lib/ruby/gems/1.8/doc/bundle-0.0.1/ri lib --title bundle-0.0.1 Documentation --quiet
  • ドキュメントの生成でエラーが出ているけど、気にしないことにした。
    • Rubyのバージョンが古いためのエラーか?
gemとバージョンの指定
  • Gemfileを初期化する。
$ cd ~/Desktop/rails116
$ bundle init
$ cat Gemfile
# A sample Gemfile
source "https://rubygems.org"

# gem "rails"
  • 作業ディレクトリ直下にGemfileが生成された。

  • Gemfileに必要なgemとバージョンを書き込む。
$ cat <<EOS >> Gemfile
gem "rails", "1.1.6"
gem "rake", "0.7.1"
gem "sqlite3"
EOS
指定したgemバージョンをインストール
  • Gemfileで指定したgemバージョンをインストールする。
$ bundle install
  • installは省略可能なので、bundleのみでもOK。
$ bundle
Fetching gem metadata from https://rubygems.org/...........
Resolving dependencies...
Installing rake 0.7.1 (was 10.3.2)
Installing activesupport 1.3.1 (was 4.1.7)
Installing actionpack 1.12.5 (was 4.1.7)
Installing actionmailer 1.2.5 (was 4.1.7)
Installing activerecord 1.14.4 (was 4.1.7)
Installing actionwebservice 1.1.6
Installing rails 1.1.6 (was 4.1.7)
Installing sqlite3 1.3.10
Using bundler 1.7.4
Your bundle is complete!
Use `bundle show [gemname]` to see where a bundled gem is installed.
  • Gemfileで指定したgemバージョンとそれに依存するgemがインストールされた。

  • bundle install済のgemバージョンのセットは、bundle listで確認できる。
$ bundle list
Gems included by the bundle:
  * actionmailer (1.2.5)
  * actionpack (1.12.5)
  * actionwebservice (1.1.6)
  * activerecord (1.14.4)
  * activesupport (1.3.1)
  * bundler (1.7.4)
  * rails (1.1.6)
  * rake (0.7.1)
  * sqlite3 (1.3.10)
  • と同時に、このリストはbundlerの管理下で利用されるgemバージョンでもある。
    • たとえRails-4.1.7やRake-10.3.2が追加インストールされたとしても、
    • bundlerの管理下で実行する限り、上記gemの利用が保証されるのだ。

  • ちなみに、bundle install(オプション指定無し)は、gemデフォルトの場所にインストールされる。
$ gem env home
/Users/zari/.rbenv/versions/1.8.7-p374/lib/ruby/gems/1.8

$ bundle show rails
/Users/zari/.rbenv/versions/1.8.7-p374/lib/ruby/gems/1.8/gems/rails-1.1.6

$ bundle show rake
/Users/zari/.rbenv/versions/1.8.7-p374/lib/ruby/gems/1.8/gems/rake-0.7.1

$ bundle show sqlite3
/Users/zari/.rbenv/versions/1.8.7-p374/lib/ruby/gems/1.8/gems/sqlite3-1.3.10
bundlerの管理下で実行
  • bundlerの管理下で実行するためには、bundle execを付加してコマンド実行する必要がある。
    • bundle execありなら、たとえRails-4.1.7やRake-10.3.2がインストールされていても、Rails-1.1.6とRake-0.7.1が利用されるのだ。
    • bundle exec無しでは、今まで同様、インストールされているgemの中で最新のバージョンが利用されてしまう...。
  • rails-1.1.6のscaffoldなら、以下のようなコマンド操作になる。
      • -d sqlite3 = データベースにsqlite3を指定
$ bundle exec rails todo -d sqlite3
$ cd todo
# config/boot.rbを修正する
$ bundle exec script/generate model todo
# db/migrate/001_create_todos.rbを修正する
$ bundle exec rake db:migrate
$ bundle exec script/generate scaffold todo
$ bundle exec script/server
  • 上記コマンド操作の途中で修正するファイルは、以下のとおり。
# config/boot.rbを修正する
@@ -25,7 +25,7 @@
       rails_gem = Gem.cache.search('rails', "=#{version}").first
 
       if rails_gem
-        require_gem "rails", "=#{version}"
+        gem "rails", "=#{version}"
         require rails_gem.full_gem_path + '/lib/initializer'
       else
         STDERR.puts %(Cannot find gem for Rails =#{version}:
@@ -35,7 +35,7 @@
         exit 1
       end
     else
-      require_gem "rails"
+      require "rails"
       require 'initializer'
     end
   end
# db/migrate/001_create_todos.rbを修正する
class CreateTodos < ActiveRecord::Migration
  def self.up
    create_table :todos do |t|
      t.column :body,       :string
      t.column :due,        :date
      t.column :done,       :boolean
    end
  end

  def self.down
    drop_table :todos
  end
end
  • ブラウザでアクセスしてみると...

f:id:zariganitosh:20141105100940p:image:w450

f:id:zariganitosh:20141105100938p:image:w450

bundlerとたった4行のGemfileで、確実に動作するRails-1.1.6環境を素早く作れた!

# Gemfile
source "https://rubygems.org"
gem "rails", "1.1.6"
gem "rake", "0.7.1"
gem "sqlite3"

デフォルトのgem

  • 以上のように、利用するgemのバージョンまで指定できるbundlerは非常に便利。
  • gemコマンド同様、常にbundleコマンドも使えるようにしておきたい。
    • 今後は、gem installに代わって、bundle installを使いたいくらい。
  • ところでbundlerもgemなので、最初にgem install bundlerが必ず必要になる。
  • OSX標準のRubyしか使っていない時なら、gem install bundlerは1回で済む。問題ない。
  • ところが、今やrbenvで必要なRuby環境をいくつでも、素早くインストール可能になった。
  • 場合によっては、実験的にインストールとアンインストールを何度も繰り返すこともある。
  • 今のままではその度にgem install bundlerを繰り返すことになってしまう...。面倒くさい。

その面倒を解決してくれるのが、rbenv-default-gemsなのだ。

  • 前回、rbenvをインストールする時にrbenv-default-gemsもインストールしておいた。
    • というより、rbenv-default-gemsの依存関係によって、rbenvもインストールしたのだった。
  • よって、~/.rbenv/default-gemsファイルに、"bundler"と書き込んでおくだけで幸せになれる。
$ echo bundler >> ~/.rbenv/default-gems
$ cat ~/.rbenv/default-gems
bundler

  • ~/.rbenv/default-gemsに書かれたgemは、rbenv installする時に、同時にインストールされる。
  • 現在のRuby-1.8.7-p375を一旦削除して、試してみた。
$ rbenv uninstall 1.8.7-p375
rbenv: remove /Users/zari/.rbenv/versions/1.8.7-p375? y

  • すかさず、再インストールしてみると...
$ rbenv install 1.8.7-p375
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/usr/include/c++/4.2.1
Checking out http://svn.ruby-lang.org/repos/ruby/branches/ruby_1_8_7...
Installing ruby-1.8.7-p375...
Installed ruby-1.8.7-p375 to /Users/zari/.rbenv/versions/1.8.7-p375

Downloading rubygems-1.6.2.tgz...
-> http://dqw8nmjcqpjn7.cloudfront.net/cb5261818b931b5ea2cb54bc1d583c47823543fcf9682f0d6298849091c1cea7
Installing rubygems-1.6.2...
Installed rubygems-1.6.2 to /Users/zari/.rbenv/versions/1.8.7-p375

Fetching: bundler-1.7.4.gem (100%)
Successfully installed bundler-1.7.4
1 gem installed
Installing ri documentation for bundler-1.7.4...
Installing RDoc documentation for bundler-1.7.4...
  • bundlerもインストールされた!
$ rbenv global 1.8.7-p375 
$ gem list

 *** LOCAL GEMS ***

bundler (1.7.4)

素晴らしい!

  • ~/.rbenv/default-gemsには、bundlerに限らず、あらゆるgemを指定できる。
  • Ruby標準になって欲しいと思うgemがあれば、それも書いておくと良さそう。

  • 以前のRuby-1.8.7-p375環境も素早く復元できた。
$ cd ~/Desktop/rails116
$ bundle
Fetching gem metadata from https://rubygems.org/..........
Installing rake 0.7.1
Installing activesupport 1.3.1
Installing actionpack 1.12.5
Installing actionmailer 1.2.5
Installing activerecord 1.14.4
Installing actionwebservice 1.1.6
Installing rails 1.1.6
Installing sqlite3 1.3.10
Using bundler 1.7.4
Your bundle is complete!
Use `bundle show [gemname]` to see where a bundled gem is installed.

$ gem list

 *** LOCAL GEMS ***

actionmailer (1.2.5)
actionpack (1.12.5)
actionwebservice (1.1.6)
activerecord (1.14.4)
activesupport (1.3.1)
bundler (1.7.4)
rails (1.1.6)
rake (0.7.1)
sqlite3 (1.3.10)

$ cd todo
$ bundle exec script/server
./script/../config/boot.rb:25:Warning: Gem::cache is deprecated and will be removed on or after August 2011.  Use Gem::source_index.
./script/../config/boot.rb:25:Warning: Gem::SourceIndex#search support for String patterns is deprecated, use #find_name
=> Booting WEBrick...
=> Rails application started on http://0.0.0.0:3000
=> Ctrl-C to shutdown server; call with --help for options
[2014-11-05 09:58:00] INFO  WEBrick 1.3.1
[2014-11-05 09:58:00] INFO  ruby 1.8.7 (2013-12-22) [i686-darwin13.4.0]
[2014-11-05 09:58:00] INFO  WEBrick::HTTPServer#start: pid=41225 port=3000
  • 何の問題もなく、さきほど作ったtodoプロジェクトが起動した。

f:id:zariganitosh:20141105100938p:image:w450

Gemfileの書き方

バージョン指定
  • Gemfileのバージョンは、もう少し柔軟に指定できる。
# rails 1.1.6を利用する
gem "rails", "1.1.6"

# rails 1.1.2以上を利用する
gem "rails", ">=1.1.2"

# rails 1.1.2以上 && 1.1系で最新のものを利用する
gem "rails", "~>1.1.2"

:source =>
  • ソース(gemをダウンロードするサーバー)を個別に指定する。
gem "rails", "1.1.6", :source => https://gems.example.com
:git =>
# masterの最新コミットをインストール
gem 'jpdate', :git => 'https://github.com/zarigani/jcal.git'

# :tag, :branch, :refなどを指定してインストール
gem 'jpdate', :git => 'https://github.com/tenderlove/nokogiri.git', :branch => '1.4'

  • ちなみに、gemコマンドのデフォルトとは異なる場所にインストールされた。
$ bundle show jpdate
/Users/zari/.rbenv/versions/1.8.7-p375/lib/ruby/gems/1.8/bundler/gems/jcal-cf7b86d88e08
:path =>
  • インストール済のgemをbundlerの管理に含める。
# ダウンロードフォルダに展開されたmecab-ruby-0.994をbundlerの管理下におく
gem "mecab-ruby", :path => '~/Downloads/ruby/1.8/gems/mecab-ruby-0.994'
詳細

プロジェクト配下にgemをインストール

  • 今まではbundle installの時、一切のオプションは指定せず、実行してきた。
  • その場合、bundlerはgemコマンドデフォルトの場所(gem env home)にインストールする。
  • 例えば、rbenvが管理するRuby-1.8.7-p375のgem env homeは、以下のように設定されている。
$ rbenv version
1.8.7-p375 (set by /Users/zari/.rbenv/version)

$ gem env home
/Users/zari/.rbenv/versions/1.8.7-p375/lib/ruby/gems/1.8

  • 様々なプロジェクトでbundle installを繰り返していると、当然ながらgem env homeの中はgemとバージョンで溢れ返る。
  • 溢れ返ったとしても、bundlerが必要なgemとバージョンを取りまとめているので、問題はないはず。
  • 但し、溢れ返って混沌とした状態のgem env homeは、何が必要で、何が不要か、もはや理解不能な状態になってくる。
  • あるプロジェクトで不要なgemも、他のプロジェクトでは必要かもしれない。また、その逆もあり得る。
  • 実験的にインストールして、もはや使わなくなったgemもあるはずなのに、削除するのが怖い...。
  • gem env homeにすべてのgemを詰め込んでしまうのは、あまり良い状態とは言えないのではないか?
  • 特に実験的なプロジェクトでgemを試用する場合は、gem env homeにはインストールしたくない。

そう言った気持ちを察してか、bundlerにはプロジェクト配下にインストールするオプションがある!


  • さっそく、todoプロジェクト配下にgemをインストールしてみる。
  • まずは以前と同様にGemfileを作成しておく。
$ cd ~/Desktop/rails116/todo
$ bundle init
Writing new Gemfile to /Users/zari/Desktop/rails116/todo/Gemfile
$ cat <<EOS >> Gemfile
gem "rails", "1.1.6"
gem "rake", "0.7.1"
gem "sqlite3"
EOS
  • そして、bundle installの時に--pathオプションを指定する。
    • オプションの値には、インストール先のパスを指定するのだ。
    • 通常は、--path=vendor/bundleが推奨されている。
$ bundle install --path=vendor/bundle
Fetching gem metadata from https://rubygems.org/...........
Resolving dependencies...
Installing rake 0.7.1
Installing activesupport 1.3.1
Installing actionpack 1.12.5
Installing actionmailer 1.2.5
Installing activerecord 1.14.4
Installing actionwebservice 1.1.6
Installing rails 1.1.6
Installing sqlite3 1.3.10
Using bundler 1.7.4
Your bundle is complete!
It was installed into ./vendor/bundle
  • todoプロジェクトのvendor/bundle以下に、gemがインストールされた!
$ find vendor/bundle/ruby/1.8/gems/* -d 0
vendor/bundle/ruby/1.8/gems/actionmailer-1.2.5
vendor/bundle/ruby/1.8/gems/actionpack-1.12.5
vendor/bundle/ruby/1.8/gems/actionwebservice-1.1.6
vendor/bundle/ruby/1.8/gems/activerecord-1.14.4
vendor/bundle/ruby/1.8/gems/activesupport-1.3.1
vendor/bundle/ruby/1.8/gems/rails-1.1.6
vendor/bundle/ruby/1.8/gems/rake-0.7.1
vendor/bundle/ruby/1.8/gems/sqlite3-1.3.10

  • 試しに、gem env homeのgemをbundler以外、すべて削除してみる。
$ gem list --no-version|grep ^[a-z]|grep -v bundler|xargs gem uninstall -aIx
Successfully uninstalled actionmailer-1.2.5
Successfully uninstalled actionpack-1.12.5
Successfully uninstalled actionwebservice-1.1.6
Successfully uninstalled activerecord-1.14.4
Successfully uninstalled activesupport-1.3.1
Removing rails
Successfully uninstalled rails-1.1.6
Removing rake
Successfully uninstalled rake-0.7.1
Successfully uninstalled sqlite3-1.3.10

$ gem list

 *** LOCAL GEMS ***

bundler (1.7.4)
  • これで、gem env homeにはbundler以外、もう何も残っていない。

  • サーバーを起動してみると...
$ bundle exec script/server
./script/../config/boot.rb:25:Warning: Gem::cache is deprecated and will be removed on or after August 2011.  Use Gem::source_index.
./script/../config/boot.rb:25:Warning: Gem::SourceIndex#search support for String patterns is deprecated, use #find_name
=> Booting WEBrick...
=> Rails application started on http://0.0.0.0:3000
=> Ctrl-C to shutdown server; call with --help for options
[2014-11-05 14:52:56] INFO  WEBrick 1.3.1
[2014-11-05 14:52:56] INFO  ruby 1.8.7 (2013-12-22) [i686-darwin13.4.0]
[2014-11-05 14:52:56] INFO  WEBrick::HTTPServer#start: pid=45168 port=3000
  • gem env homeの中は空っぽだけど、
  • それでもtodoプロジェクトは動いた!

f:id:zariganitosh:20141105100938p:image:w450


bundle execによって、vendor/bundleのgemが利用されているのだ!

--path=vendor/bundleを保持する仕組み
  • 一旦--path=vendor/bundleオプションを指定すると、
  • 作業ディレクトリには、.bundle/configが作成される。
$ cat .bundle/config
---
BUNDLE_PATH: vendor/bundle
BUNDLE_DISABLE_SHARED_GEMS: '1'
      • BUNDLE_PATH: vendor/bundleは、--path=vendor/bundleオプションの保存。
      • BUNDLE_DISABLE_SHARED_GEMS: '1'は、既存のgemが存在しても共有せず、vendor/bundleへコピーする設定。

  • .bundle/configの内容を確認するbundle configコマンドもある。
$ bundle config
Settings are listed in order of priority. The top value will be used.
path
Set for your local app (/Users/zari/Desktop/rails116/todo/.bundle/config): "vendor/bundle"

disable_shared_gems
Set for your local app (/Users/zari/Desktop/rails116/todo/.bundle/config): "1"

  • 上記のように、--pathオプションを指定すると.bundle/configに保持されるので、
  • その後はbundle installのみでも、--path=vendor/bundleへのインストールとなる。
$ bundle install
Using rake 0.7.1
Using activesupport 1.3.1
Using actionpack 1.12.5
Using actionmailer 1.2.5
Using activerecord 1.14.4
Using actionwebservice 1.1.6
Using rails 1.1.6
Using sqlite3 1.3.10
Using bundler 1.7.4
Your bundle is complete!
It was installed into ./vendor/bundle
--pathオプションを解除する
  • では、プロジェクトごとのbundle管理をやめて、再びgem env homeでbundle管理するには?
  • --systemオプションを指定すると、gem env homeにインストールされるのだ。
$ bundle install --system
Fetching gem metadata from https://rubygems.org/..........
Installing rake 0.7.1
Installing activesupport 1.3.1
Installing actionpack 1.12.5
Installing actionmailer 1.2.5
Installing activerecord 1.14.4
Installing actionwebservice 1.1.6
Installing rails 1.1.6
Installing sqlite3 1.3.10
Using bundler 1.7.4
Your bundle is complete!
Use `bundle show [gemname]` to see where a bundled gem is installed.

$ gem list

 *** LOCAL GEMS ***

actionmailer (1.2.5)
actionpack (1.12.5)
actionwebservice (1.1.6)
activerecord (1.14.4)
activesupport (1.3.1)
bundler (1.7.4)
rails (1.1.6)
rake (0.7.1)
sqlite3 (1.3.10)

  • そして、.bundle/configの内容は削除された。
$ cat .bundle/config
 --- {}

$ bundle config
Settings are listed in order of priority. The top value will be used.
  • 以降は、--pathオプションを指定しない限り、常にgem env homeへのインストールとなるのだ。

Railsプロジェクトの遅延作成

遅延というキーワードは崇高な気がするが、とっても単純な話。

  • 例えば、rails todoコマンドでプロジェクトを始めようとする時の悩み。
    • 実験的なプロジェクトなので、todoプロジェクトフォルダ内でbundleを管理したい。
    • それにはtodoフォルダ内でbundle install --path=vendor/bundleする必要がある。
    • そこでrails todoによって、todoプロジェクトフォルダを生成しようとすると、railsコマンドがないよ、ってエラーで警告されてしまう...。
  • Railsを、自身が生成するプロジェクト内に含めてbundle管理することはできないのだろうか?

以下の手順でRails自身のプロジェクト内でbundle管理できた!

$ mkdir todo
$ cd todo
$ bundle init
Writing new Gemfile to /Users/zari/Desktop/rails116/todo/Gemfile

$ cat <<EOS >> Gemfile
gem "rails", "1.1.6"
gem "rake", "0.7.1"
gem "sqlite3"
EOS

$ bundle install --path=vendor/bundle
Fetching gem metadata from https://rubygems.org/...........
Resolving dependencies...
Installing rake 0.7.1
Installing activesupport 1.3.1
Installing actionpack 1.12.5
Installing actionmailer 1.2.5
Installing activerecord 1.14.4
Installing actionwebservice 1.1.6
Installing rails 1.1.6
Installing sqlite3 1.3.10
Using bundler 1.7.4
Your bundle is complete!
It was installed into ./vendor/bundle

$ bundle exec rails . -d sqlite3
  • 先にmkdir todoして、bundle環境を整えてから、railsコマンドを使うのだ!
  • railsコマンドの引数には、todoフォルダ内で . 指定するのがポイント。

  • 以降の手順は今までどおり。
# config/boot.rbを修正する
$ bundle exec script/generate model todo
# db/migrate/001_create_todos.rbを修正する
$ bundle exec rake db:migrate
$ bundle exec script/generate scaffold todo
$ bundle exec script/server

2014-10-31

古いRails環境を再起動するまで

古いRails環境というのは、具体的にはRails1.1.6である。実はこのブログの始まりは、Railsの学習の記録であった。むかしむかし、このブログ創世の頃、Railsはバージョン1.1.6がメジャーであった。そのため、自分のMacBookの中には、Rails1.1.6の実験プロジェクトがたくさんある。

しかし、現在のRailsはバージョン4.1.7まで進んでしまった。過去のプロジェクトをちょっと動かして試してみたい衝動に駆られても、エラー出まくり。まったく動かない...。このまま永遠に起動しないRailsプロジェクトが残り続けるのも悲しいので、昔の環境を整えて、過去の遺産を再起動してみようと思い立った。以下は、MacBook Pro Retina15 OSX 10.9.5の環境で、Rails1.1.6を起動するまでの記録。

HomebrewとCommand Line Toolsを使う

  • rbenvをインストールするために、Homebrewを使った。
  • 自分のMacBook環境に既にインストール済みだったので。

$ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

  • Command Line Toolsのインストール
    • Homebrewがビルドする時に必要になるので。
$ xcode-select --install
    • 入れないで使いだすと、入れてねって言われる。
Warning: No developer tools installed.
You should install the Command Line Tools.
Run `xcode-select --install` to install them.

rbenv環境をインストール

  • rbenvは、複数のRuby環境を素早く切り替えて使い分けるツール。
  • rbenvを快適に使うためには、以下二つのgemも入れた方が良さそう。
    • rbenv-default-gems(新規ruby環境インストール時に、default-gemsに設定したgemもインストールしてくれる)
    • rbenv-gem-rehash(ruby環境切替時に必要なrbenv rehashコマンドを良きに計らい実行してくれる)
    • どちらもrbenvと同じ作者の方が開発しているツールのようだ。
  • 依存関係によって、以下のコマンドで必要なrbenv環境がインストールされる。
$ brew install rbenv-default-gems rbenv-gem-rehash
🍺  /usr/local/Cellar/rbenv/0.4.0: 31 files, 152K, built in 2 seconds
🍺  /usr/local/Cellar/ruby-build/20141016: 129 files, 552K, built in 2 seconds
🍺  /usr/local/Cellar/rbenv-default-gems/1.0.0: 4 files, 16K, built in 2 seconds
🍺  /usr/local/Cellar/rbenv-gem-rehash/1.0.0: 7 files, 24K, built in 2 seconds

rbenv、ruby-build、rbenv-default-gems、rbenv-gem-rehashがインストールされた!

rbenvを使う準備

  • そのターミナル環境でrbenvを使うためには、最初に必ずeval "$(rbenv init -)"しておく必要がある。
$ eval "$(rbenv init -)"

  • また上記コマンドは、ターミナルでタブやウィンドウを開く度に毎回必要になるので、使っているシェル環境の設定ファイルに追記しておくのだ。
    • 自分の場合は、bashを使っているので、~/.bashrcに追記しておいた。
    • ちなみに自分の環境は、~/.bash_profileが~/.bashrcを読み込む設定になっている。
$ echo 'eval "$(rbenv init -)"' >> ~/.bashrc

  • まだ、OSXデフォルトのRuby環境しかない。
 $ rbenv versions
 * system (set by /Users/zari/.rbenv/version)

Ruby-1.8.7をインストール

  • rbenvでインストール可能なRubyのバージョンを調べる。
$ rbenv install -l
Available versions:
  1.8.6-p383
  1.8.6-p420
  1.8.7-p249
  1.8.7-p302
  1.8.7-p334
  1.8.7-p352
  1.8.7-p357
  1.8.7-p358
  1.8.7-p370
  1.8.7-p371
  1.8.7-p374
  1.8.7-p375
  1.9.1-p378
  1.9.1-p430
  1.9.2-p0
...中略...
  • ズラッとリスト表示されて迷う。
  • Rails1.1.6当時のRubyは1.8世代。
  • よって1.8系の最終パッチの1.8.7-p375に決めた。

  • しかし、インストールするとエラーが発生...。
$ rbenv install 1.8.7-p375
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/usr/include/c++/4.2.1

ERROR: This package must be compiled with GCC, but ruby-build couldn't
find a suitable `gcc` executable on your system. Please install GCC
and try again.

DETAILS: Apple no longer includes the official GCC compiler with Xcode
as of version 4.2. Instead, the `gcc` executable is a symlink to
`llvm-gcc`, a modified version of GCC which outputs LLVM bytecode.

For most programs the `llvm-gcc` compiler works fine. However,
versions of Ruby older than 1.9.3-p125 are incompatible with
`llvm-gcc`. To build older versions of Ruby you must have the official
GCC compiler installed on your system.

TO FIX THE PROBLEM: Install Homebrew's apple-gcc42 package with this
command: brew tap homebrew/dupes ; brew install apple-gcc42

You will need to install the official GCC compiler to build older
versions of Ruby even if you have installed Apple's Command Line Tools
for Xcode package. The Command Line Tools for Xcode package only
includes `llvm-gcc`.

BUILD FAILED (OS X 10.9.5 using ruby-build 20141016)

  • エラーメッセージを真剣に読むと...
    • どうやらXcode4.2以降は、gccはllvm-gccを示すシンボルリンクになってしまったと。
    • そのため、バージョン1.9.3-p125より古いRubyは、ビルドできなくなってしまったと。
    • 古いRubyをビルドするには、GCCコンパイラーが必要になると。
  • どうやら、以下のコマンドでHomebrewでgccをインストールするようだ。
$ brew tap homebrew/dupes ; brew install apple-gcc42
🍺  /usr/local/Cellar/apple-gcc42/4.2.1-5666.3: 104 files, 75M, built in 2 seconds

gcc-4.2、インストール完了!


  • 気を取り直して、もう一度Ruby-1.8.7-p375をインストール。
$ rbenv install 1.8.7-p375
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/usr/include/c++/4.2.1
Checking out http://svn.ruby-lang.org/repos/ruby/branches/ruby_1_8_7...
Installing ruby-1.8.7-p375...
Installed ruby-1.8.7-p375 to /Users/zari/.rbenv/versions/1.8.7-p375

Downloading rubygems-1.6.2.tgz...
-> http://dqw8nmjcqpjn7.cloudfront.net/cb5261818b931b5ea2cb54bc1d583c47823543fcf9682f0d6298849091c1cea7
Installing rubygems-1.6.2...
Installed rubygems-1.6.2 to /Users/zari/.rbenv/versions/1.8.7-p375

Ruby-1.8.7-p375、rubygems-1.6.2、インストール完了!

Ruby環境の切替

  • さっそく、Ruby-1.8.7-p375に切り替えてみる。
    • 先頭の*が現在選択されているRuby環境を示すマーク。
 $ rbenv versions
 * system (set by /Users/zari/.rbenv/version)
   1.8.7-p375

 $ rbenv global 1.8.7-p375

 $ rbenv versions
   system
 * 1.8.7-p375 (set by /Users/zari/.rbenv/version)

  • rubyバージョンとgemのインストール先が、ちゃんと変更されていることを確認する。
$ ruby -v
ruby 1.8.7 (2013-12-22 patchlevel 375) [i686-darwin13.4.0]

$ gem env home
/Users/zari/.rbenv/versions/1.8.7-p375/lib/ruby/gems/1.8
  • もし、上記のように変更されていなかったら、rbenv rehashコマンドを実行してみる。
    • 本来は、rbenv-gem-rehashが良きに計らい自動で実行してくれるはず。
    • rbenvインストール後の初回の切替のみ、rbenv rehashの実行が必要になるかもしれない。

railsをインストール

  • rbenvで切り替えたRuby-1.8.7-p375環境にRailsをインストールしてみる。

  • Railsのバージョンを確認してみる。
$ gem search ^rails$ -ra

 *** REMOTE GEMS ***

rails (4.1.7, 4.1.6, 4.1.5, 4.1.4, 4.1.3, 4.1.2, 4.1.1, 4.1.0, 4.0.11, 4.0.10, 4.0.9, 4.0.8, 4.0.7, 4.0.6, 4.0.5, 4.0.4, 4.0.3, 4.0.2, 4.0.1, 4.0.0, 3.2.20, 3.2.19, 3.2.18, 3.2.17, 3.2.16, 3.2.15, 3.2.14, 3.2.13, 3.2.12, 3.2.11, 3.2.10, 3.2.9, 3.2.8, 3.2.7, 3.2.6, 3.2.5, 3.2.4, 3.2.3, 3.2.2, 3.2.1, 3.2.0, 3.1.12, 3.1.11, 3.1.10, 3.1.9, 3.1.8, 3.1.7, 3.1.6, 3.1.5, 3.1.4, 3.1.3, 3.1.2, 3.1.1, 3.1.0, 3.0.20, 3.0.19, 3.0.18, 3.0.17, 3.0.16, 3.0.15, 3.0.14, 3.0.13, 3.0.12, 3.0.11, 3.0.10, 3.0.9, 3.0.8, 3.0.7, 3.0.6, 3.0.5, 3.0.4, 3.0.3, 3.0.2, 3.0.1, 3.0.0, 2.3.18, 2.3.17, 2.3.16, 2.3.15, 2.3.14, 2.3.12, 2.3.11, 2.3.10, 2.3.9, 2.3.8, 2.3.7, 2.3.6, 2.3.5, 2.3.4, 2.3.3, 2.3.2, 2.2.3, 2.2.2, 2.1.2, 2.1.1, 2.1.0, 2.0.5, 2.0.4, 2.0.2, 2.0.1, 2.0.0, 1.2.6, 1.2.5, 1.2.4, 1.2.3, 1.2.2, 1.2.1, 1.2.0, 1.1.6, 1.1.5, 1.1.4, 1.1.3, 1.1.2, 1.1.1, 1.1.0, 1.0.0, 0.14.4, 0.14.3, 0.14.2, 0.14.1, 0.13.1, 0.13.0, 0.12.1, 0.12.0, 0.11.1, 0.11.0, 0.10.1, 0.10.0, 0.9.5, 0.9.4.1, 0.9.4, 0.9.3, 0.9.2, 0.9.1, 0.9.0, 0.8.5, 0.8.0)
  • ズラッとRailsのバージョンが並んだ。
  • 欲しいバージョン1.1.6もちゃんとある。

  • Rails-1.1.6をインストール。
$ gem install rails -v 1.1.6
Fetching: rake-10.3.2.gem (100%)
Fetching: activesupport-1.3.1.gem (100%)
Fetching: activerecord-1.14.4.gem (100%)
Fetching: actionpack-1.12.5.gem (100%)
Fetching: actionmailer-1.2.5.gem (100%)
Fetching: actionwebservice-1.1.6.gem (100%)
Fetching: rails-1.1.6.gem (100%)
Successfully installed rake-10.3.2
Successfully installed activesupport-1.3.1
Successfully installed activerecord-1.14.4
Successfully installed actionpack-1.12.5
Successfully installed actionmailer-1.2.5
Successfully installed actionwebservice-1.1.6
Successfully installed rails-1.1.6
7 gems installed
Installing ri documentation for rake-10.3.2...
Installing ri documentation for activesupport-1.3.1...
ERROR:  While generating documentation for activesupport-1.3.1
... MESSAGE:   Unhandled special: Special: type=17, text=""
... RDOC args: --ri --op /Users/zari/.rbenv/versions/1.8.7-p375/lib/ruby/gems/1.8/doc/activesupport-1.3.1/ri lib --title activesupport-1.3.1 Documentation --quiet
  • インストールできたようだ。
    • ドキュメントのインストールでエラーが出ているけど、気にしないことにした。

  • データベースはsqlite3を使うつもりなので、それもインストール。
    • 正確にはrubyからsqlite3コマンドを利用するgemパッケージなのだ。
$ gem install sqlite3
Fetching: sqlite3-1.3.10.gem (100%)
Building native extensions.  This could take a while...
Successfully installed sqlite3-1.3.10
1 gem installed
...中略...
    • sqlite3コマンド自体はOSXに標準インストールされている。
$ which sqlite3
/usr/bin/sqlite3

懐かしのscaffold

  • todoプロジェクトの開始!
$ rails todo
      create  
      create  app/controllers
      create  app/helpers
      create  app/models
      create  app/views/layouts
      create  config/environments
      create  components
      create  db
      create  doc
      create  lib
      create  lib/tasks
      create  log
      create  public/images
      create  public/javascripts
      create  public/stylesheets
      create  script/performance
      create  script/process
      create  test/fixtures
      create  test/functional
      create  test/integration
      create  test/mocks/development
      create  test/mocks/test
      create  test/unit
      create  vendor
      create  vendor/plugins
      create  tmp/sessions
      create  tmp/sockets
      create  tmp/cache
      create  Rakefile
      create  README
      create  app/controllers/application.rb
      create  app/helpers/application_helper.rb
      create  test/test_helper.rb
      create  config/database.yml
      create  config/routes.rb
      create  public/.htaccess
      create  config/boot.rb
      create  config/environment.rb
      create  config/environments/production.rb
      create  config/environments/development.rb
      create  config/environments/test.rb
      create  script/about
      create  script/breakpointer
      create  script/console
      create  script/destroy
      create  script/generate
      create  script/performance/benchmarker
      create  script/performance/profiler
      create  script/process/reaper
      create  script/process/spawner
      create  script/runner
      create  script/server
      create  script/plugin
      create  public/dispatch.rb
      create  public/dispatch.cgi
      create  public/dispatch.fcgi
      create  public/404.html
      create  public/500.html
      create  public/index.html
      create  public/favicon.ico
      create  public/robots.txt
      create  public/images/rails.png
      create  public/javascripts/prototype.js
      create  public/javascripts/effects.js
      create  public/javascripts/dragdrop.js
      create  public/javascripts/controls.js
      create  public/javascripts/application.js
      create  doc/README_FOR_APP
      create  log/server.log
      create  log/production.log
      create  log/development.log
      create  log/test.log

$ cd todo

$ script/generate model todo
./script/../config/boot.rb:25:Warning: Gem::cache is deprecated and will be removed on or after August 2011.  Use Gem::source_index.
./script/../config/boot.rb:25:Warning: Gem::SourceIndex#search support for String patterns is deprecated, use #find_name
./script/../config/boot.rb:28: undefined method `require_gem' for main:Object (NoMethodError)
	from script/generate:2:in `require'
	from script/generate:2
  • エラーで止まった。

  • エラー箇所を確認してみると、require_gemの部分。
  • たしかgem1.0以降、require_gemは廃止されている。
  • たぶん、そのせい。TextMateで以下のように修正してみた。
$ mate config/boot.rb
$ diff -u <(pbpaste) config/boot.rb
--- /dev/fd/63	2014-10-30 17:28:00.000000000 +0900
+++ config/boot.rb	2014-10-30 17:27:43.000000000 +0900
@@ -25,7 +25,7 @@
       rails_gem = Gem.cache.search('rails', "=#{version}").first
 
       if rails_gem
-        require_gem "rails", "=#{version}"
+        gem "rails", "=#{version}"
         require rails_gem.full_gem_path + '/lib/initializer'
       else
         STDERR.puts %(Cannot find gem for Rails =#{version}:
@@ -35,7 +35,7 @@
         exit 1
       end
     else
-      require_gem "rails"
+      require "rails"
       require 'initializer'
     end
   end

  • 気を取り直して、script/generate model todoからやり直し。
$ script/generate model todo
./script/../config/boot.rb:25:Warning: Gem::cache is deprecated and will be removed on or after August 2011.  Use Gem::source_index.
./script/../config/boot.rb:25:Warning: Gem::SourceIndex#search support for String patterns is deprecated, use #find_name
/Users/zari/.rbenv/versions/1.8.7-p375/lib/ruby/gems/1.8/gems/rails-1.1.6/lib/rails_generator/lookup.rb:199:Warning: Gem::cache is deprecated and will be removed on or after August 2011.  Use Gem::source_index.
/Users/zari/.rbenv/versions/1.8.7-p375/lib/ruby/gems/1.8/gems/rails-1.1.6/lib/rails_generator/lookup.rb:199:Warning: Gem::SourceIndex#search support for Regexp patterns is deprecated, use #find_name
      exists  app/models/
      exists  test/unit/
      exists  test/fixtures/
      create  app/models/todo.rb
      create  test/unit/todo_test.rb
      create  test/fixtures/todos.yml
      create  db/migrate
      create  db/migrate/001_create_todos.rb
  • 今度は正常に終了した。

  • データベースの設定
    • TextMateで以下のように修正した。
$ mate config/database.yml
$ diff -u <(pbpaste) config/database.yml
--- /dev/fd/63	2014-10-30 17:34:05.000000000 +0900
+++ config/database.yml	2014-10-30 17:34:02.000000000 +0900
@@ -11,25 +11,19 @@
 # And be sure to use new-style password hashing:
 #   http://dev.mysql.com/doc/refman/5.0/en/old-client.html
 development:
-  adapter: mysql
-  database: todo_development
-  username: root
-  password:
-  host: localhost
+  adapter: sqlite3
+  database: db/development.sqlite3
+  timeout: 5000
 
 # Warning: The database defined as 'test' will be erased and
 # re-generated from your development database when you run 'rake'.
 # Do not set this db to the same as development or production.
 test:
-  adapter: mysql
-  database: todo_test
-  username: root
-  password:
-  host: localhost
+  adapter: sqlite3
+  database: db/test.sqlite3
+  timeout: 5000
 
 production:
-  adapter: mysql
-  database: todo_production
-  username: root
-  password: 
-  host: localhost
+  adapter: sqlite3
+  database: db/production.sqlite3
+  timeout: 5000

  • DBマイグレーションの設定
    • TextMateで以下のように修正した。
$ mate db/migrate/001_create_todos.rb
$ diff -u <(pbpaste) db/migrate/001_create_todos.rb
--- /dev/fd/63	2014-10-30 17:30:05.000000000 +0900
+++ db/migrate/001_create_todos.rb	2014-10-30 17:29:22.000000000 +0900
@@ -1,7 +1,12 @@
 class CreateTodos < ActiveRecord::Migration
   def self.up
     create_table :todos do |t|
-      # t.column :name, :string
+      t.column :body,       :string
+      t.column :due,        :date
+      t.column :done,       :boolean
+      
+      t.column :created_at, :datetime
+      t.column :updated_at, :datetime
     end
   end

  • マイグレーション実行。
$ rake db:migrate
/Users/zari/Desktop/rails116/todo/config/boot.rb:25:Warning: Gem::cache is deprecated and will be removed on or after August 2011.  Use Gem::source_index.
/Users/zari/Desktop/rails116/todo/config/boot.rb:25:Warning: Gem::SourceIndex#search support for String patterns is deprecated, use #find_name
rake aborted!
ERROR: 'rake/rdoctask' is obsolete and no longer supported. Use 'rdoc/task' (available in RDoc 2.4.2+) instead.
/Users/zari/Desktop/rails116/todo/Rakefile:8
(See full trace by running task with --trace)
  • しかし、エラーが出てしまった...。

  • 散々悩んだあげく、rakeをインストールし直してみようと削除して気付いた。
  • Rails1.1.6には、rakeのバージョンが進み過ぎているのかもしれない!
  • というわけで、削除した時に表示された(>=0.7.1)を信じて、再インストール。
$ gem uninstall rake

You have requested to uninstall the gem:
	rake-10.3.2
rails-1.1.6 depends on [rake (>= 0.7.1)]
If you remove this gems, one or more dependencies will not be met.
Continue with Uninstall? [Yn]  Y
Remove executables:
	rake

in addition to the gem? [Yn]  Y
Removing rake
Successfully uninstalled rake-10.3.2

$ gem install rake -v 0.7.1
Fetching: rake-0.7.1.gem (100%)
Successfully installed rake-0.7.1
1 gem installed
Installing ri documentation for rake-0.7.1...
Installing RDoc documentation for rake-0.7.1...

  • 気を取り直して、rake db:migrateを実行すると、正常に完了した!
$ rake db:migrate
(in /Users/zari/Desktop/rails116/todo)
/Users/zari/Desktop/rails116/todo/config/boot.rb:25:Warning: Gem::cache is deprecated and will be removed on or after August 2011.  Use Gem::source_index.
/Users/zari/Desktop/rails116/todo/config/boot.rb:25:Warning: Gem::SourceIndex#search support for String patterns is deprecated, use #find_name
 == CreateTodos: migrating =====================================================
 -- create_table(:todos)
    -> 0.0012s
 == CreateTodos: migrated (0.0012s) ============================================

  • いよいよ、最後の仕上げscaffoldだ。
$ script/generate scaffold todo
./script/../config/boot.rb:25:Warning: Gem::cache is deprecated and will be removed on or after August 2011.  Use Gem::source_index.
./script/../config/boot.rb:25:Warning: Gem::SourceIndex#search support for String patterns is deprecated, use #find_name
/Users/zari/.rbenv/versions/1.8.7-p375/lib/ruby/gems/1.8/gems/rails-1.1.6/lib/rails_generator/lookup.rb:199:Warning: Gem::cache is deprecated and will be removed on or after August 2011.  Use Gem::source_index.
/Users/zari/.rbenv/versions/1.8.7-p375/lib/ruby/gems/1.8/gems/rails-1.1.6/lib/rails_generator/lookup.rb:199:Warning: Gem::SourceIndex#search support for Regexp patterns is deprecated, use #find_name
      exists  app/controllers/
      exists  app/helpers/
      create  app/views/todos
      exists  test/functional/
  dependency  model
/Users/zari/.rbenv/versions/1.8.7-p375/lib/ruby/gems/1.8/gems/rails-1.1.6/lib/rails_generator/lookup.rb:199:Warning: Gem::cache is deprecated and will be removed on or after August 2011.  Use Gem::source_index.
/Users/zari/.rbenv/versions/1.8.7-p375/lib/ruby/gems/1.8/gems/rails-1.1.6/lib/rails_generator/lookup.rb:199:Warning: Gem::SourceIndex#search support for Regexp patterns is deprecated, use #find_name
      exists    app/models/
      exists    test/unit/
      exists    test/fixtures/
   identical    app/models/todo.rb
   identical    test/unit/todo_test.rb
   identical    test/fixtures/todos.yml
      create  app/views/todos/_form.rhtml
      create  app/views/todos/list.rhtml
      create  app/views/todos/show.rhtml
      create  app/views/todos/new.rhtml
      create  app/views/todos/edit.rhtml
      create  app/controllers/todos_controller.rb
      create  test/functional/todos_controller_test.rb
      create  app/helpers/todos_helper.rb
      create  app/views/layouts/todos.rhtml
      create  public/stylesheets/scaffold.css
  • 正常にscaffoldされたようだ!

  • サーバーを起動してみる。
$ script/server
./script/../config/boot.rb:25:Warning: Gem::cache is deprecated and will be removed on or after August 2011.  Use Gem::source_index.
./script/../config/boot.rb:25:Warning: Gem::SourceIndex#search support for String patterns is deprecated, use #find_name
=> Booting WEBrick...
=> Rails application started on http://0.0.0.0:3000
=> Ctrl-C to shutdown server; call with --help for options
[2014-10-31 14:15:02] INFO  WEBrick 1.3.1
[2014-10-31 14:15:02] INFO  ruby 1.8.7 (2013-12-22) [i686-darwin13.4.0]
[2014-10-31 14:15:02] INFO  WEBrick::HTTPServer#start: pid=37400 port=3000
  • 正常に稼働しているみたい。

ブラウザで動作確認

ちゃんと動いた!

f:id:zariganitosh:20141031142614p:image:w450

f:id:zariganitosh:20141031142613p:image:w450

f:id:zariganitosh:20141031142611p:image:w450

f:id:zariganitosh:20141031142610p:image:w450

f:id:zariganitosh:20141031142608p:image:w450

f:id:zariganitosh:20141031142657p:image:w450


稼働環境は以下のとおり

$ gem list

 *** LOCAL GEMS ***

actionmailer (1.2.5)
actionpack (1.12.5)
actionwebservice (1.1.6)
activerecord (1.14.4)
activesupport (1.3.1)
rails (1.1.6)
rake (0.7.1)
sqlite3 (1.3.10)

2012-03-09

GitHubからのSSH脆弱性メールにどのように対応すればいいのか?

  • 2012/3/8 3:28に、GitHubから一通のメールが届いた。
  • タイトルは「Action Required - SSH Key Vulnerability」
  • 自分なりに適当に訳すと「行動を求めます - SSHキーの脆弱性
  • 何だか結構重要なことが書いてあるようなことは分かる。
  • しかし、内容が全部英語なので、ネイティブジャパニーズな自分には正確に意味がとらえられず、不安が...。
A security vulnerability was recently discovered that made it possible for an attacker to add new SSH keys to arbitrary GitHub user accounts. 
最近、セキュリティ上の脆弱性が発見されました。それは、攻撃者が任意のGitHubユーザーアカウントに新規のSSHキーを追加可能というものです。

This would have provided an attacker with clone/pull access to repositories with read permissions, and clone/pull/push access to repositories with write permissions. 
この脆弱性は、読み取り可能な権限のリポジトリではclone/pullアクセスを、書き込み可能な権限のリポジトリではclone/pull/pushアクセスを、攻撃者に許してしまいます。

As of 5:53 PM UTC on Sunday, March 4th the vulnerability no longer exists.
3/4(日)午後5:53 UTC現在、もうこの脆弱性はなくなっています。

While no known malicious activity has been reported, we are taking additional precautions by forcing an audit of all existing SSH keys.
悪意のある行為は報告されていませんが、存在するすべてのSSHキーを検査してもらう、という特別な予防措置を講じています。

# Required Action
行動を求めます。

Since you have one or more SSH keys associated with your GitHub account you must visit https://github.com/settings/ssh/audit to approve each valid SSH key.
自分のGitHubアカウントに一つ以上のSSHキーを持っているなら、https://github.com/settings/ssh/audit を開いて、正当なSSHであるかを承認してください。

Until you have approved your SSH keys, you will be unable to clone/pull/push your repositories over SSH.
SSHキーを承認するまで、SSH経由でリポジトリへのclone/pull/pushができなくなります。

# Status
状況

We take security seriously and recognize this never should have happened.
GitHub Teamは、セキュリティーを真剣に考え、これは決して起きてはならないことだと思っています。

 In addition to a full code audit, we have taken the following measures to enhance the security of your account:
すべてのコードの精査に加え、セキュリティー向上のために次の対策をとりました。

 - We are forcing an audit of all existing SSH keys
存在するすべてのSSHキーを承認してもらいます。
 - Adding a new SSH key will now prompt for your password
現在は、新規のSSHキーを追加する時、パスワードの入力を促します。
 - We will now email you any time a new SSH key is added to your account
現在は、新規のSSHキーが追加されるといつでも、そのアカウント宛にメールを送信します。
 - You now have access to a log of account changes in your Account Settings page
現在は、アカウントの設定ページで、複数のアカウントの変更アカウントの変更履歴にアクセスできます。
Sincerely, The GitHub Team

やるべきこと

  • メール中のリンク https://github.com/settings/ssh/audit をクリックして、SSHキーの承認ページを開く。
  • 表示されたSSHキーに対して、
    • 正しいSSHキーなら緑色の[Approve]ボタンを押す。
    • 身に覚えのないSSHキーなら赤字の[Reject]ボタンを押す。

そのSSHキーが正しいかどうか?

  • そんなこと急に言われても、遥か昔に登録したSSHキーのことなんて、すっかり忘れてしまった...。
  • 正しいのか、不正なのか、どうやって見分けるのか?分からん!
  • とりあえず、~/.ssh に移動してlsしてみる。
$ ls
id_rsa       id_rsa.pub   known_hosts

  • その中身を見てみる。
$ cat id_rsa.pub
ssh-rsa XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX== zari@zari-MacBook.local

$ cat id_rsa
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: DES-EDE3-CBC,XXXXXXXXXXXXXXXX

xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
-----END RSA PRIVATE KEY-----
  • しかし、GitHubの確認ページに表示されるコロン区切りの文字列は、どこにも見当たらない...。

ダメだ、こりゃ...。

フィンガープリントだった

  • 表示されているのは、どうやらフィンガープリントらしい。
  • でも、フィンガープリントって、どうやってみるの?
$ ssh-keygen -l
Enter file in which the key is (/Users/zari/.ssh/id_rsa): #()内のパスでOKなので、何も入力せずreturnキーを押した
2048 XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX /Users/zari/.ssh/id_rsa.pub (RSA)
  • あるいは...
$ ssh-add -l
2048 XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX /Users/zari/.ssh/id_rsa (RSA)

全桁を確認するのが面倒

  • 桁数が多いので、確認するのが結構たいへん...
  • XX:XX:...の部分をコピーして、SSHキーの承認ページで、Safariのページ内検索してみた。
  • 一致しているキーだったら、必ずヒットするはず!
  • ヒットしたので、気持ち良く緑色の[Approve]ボタンを押した。

f:id:zariganitosh:20120309115354p:image

これでSSHアクセスできるようになった!

参考ページ

素晴らしい情報に感謝です!

流石です!

2009-07-16

Rubyのハッシュテーブルの仕組みを徹底的に理解する

ハッシュとは

一般的に理解すると抽象的で分かり難くなってしまうが、ハッシュとは、あるデータから、一定の計算をして求めた、目的に沿った数値、と思っている。それでは、どのような目的に利用されるのか?自分の知識で考えてみた。

  • 暗号化
    • webアプリケーション等で、パスワードをDBに保存する時、生のパスワードをハッシュに変換して保存する。
    • Digest::SHA1.hexdigest等で求めたハッシュから、元のデータを復元するのが非常に困難という特性を利用する。
    • 保存しているパスワードハッシュが、たとえ漏洩したとしても、不正利用を防止できる。
    • パスワードを照合するときも、ハッシュに変換して、保存しているパスワードハッシュと一致するかどうかで判断する。
  • 同等の確認
    • 長い文字列データを比較する時、全ての文字が等しいかチェックするのは非常に時間がかかる。
    • しかし、長い文字列データをハッシュに変換しておき、ハッシュ同士を数値として比較すれば効率が良い。
    • 改竄のチェックとか、チェックデジットとして利用してデータ通信時の文字化け検出などの利用が考えられる。
  • ハッシュテーブルによる高速検索
    • ハッシュテーブルとは、キーに対応する値を、瞬時に取り出すことができるテーブル。
    • キーと値をペアにしたテーブルを検索する時、if文を使って先頭から順に照合していくと、以下の問題が発生する。
      • キーの位置が最初か、最後かによって検索時間が変わってくる。
      • キーが増える程に、平均の検索時間も増加する。
    • この問題を解決するには、if文によるキーの照合を止める必要がある。
    • キーをインデックス(先頭から何番目のデータかを表現する数値)に変換すれば、対応する値の格納位置を特定できる。
    • キーが何万件あろうと、インデックスを見れば、対応するデータを瞬時に取り出せるのだ。

今回、理解しようと思っているのは、このハッシュテーブルの仕組み。Rubyでは{:apple => 120, :orange => 100}のように表現すれば、Hashオブジェクトが生成され、簡単に利用できる。便利なのでよく使うが、その裏で、どのような仕組みで実現されているか、気にしたことが無かった。当初、if文で地道に検索しているんだろうな、位にしか考えていなかった。とんでもない!もっと素晴らしく効率的な仕組みなのに。

Rubyでは、Hashオブジェクトに限らず、予約語の検索でも完全ハッシュ関数を利用して効率的に処理されているらしい。ハッシュテーブルを理解すれば、より効率的な処理方法を発想できるかもしれない。物事を効率的に処理する重要な技術なのだ。

Rubyのソースコードと解説

ソースコードと解説は以下のリンクから取得した。解説されているのは、ruby 1.7.3 2002-09-12版だが、ハッシュテーブルの仕組みに大きな変化は無さそうなので、1.8.7でも大変参考になる。

ハッシュテーブル

前回の日記:どのようにして一番右の1のビット位置を求めているのか?で理解した、M系列を活用する方法も、ハッシュテーブルを利用していると言える。

  • 1、2、4、8、16、...、9223372036854775808(2の63乗)という64ビットの2のn乗数値を、0から63の数値(ハッシュ)に変換している。
  • 変換する時に重複は無く、64ビットの2のn乗数値は、必ず0から63のたった一つの値と対応している。
  • このように重複の無いハッシュを得られることを完全ハッシュと言う。
  • しかもこの場合、変換される数値も0から63で、変換前の要素数とぴったり一致していて無駄が無い。
  • このような完全無駄無しハッシュが、単純な演算だけで実現できてしまうシンプルさが凄いところ。

一方、M系列を利用しないで、Rubyのハッシュで実装しても、やはりハッシュテーブルが利用される訳だ。目に見えるRubyコードは、ものすごくシンプル。しかし、内部的なC言語の処理は遥かに複雑だ。ハッシュに変換する方法も、ハッシュテーブルの構造も。それは、あらゆるキーをハッシュに変換して、ハッシュは重複する可能性もあり、それでも的確に検索できるようにしているためだ。

  • Rubyのハッシュテーブルは、単純な配列ではない。st_tableという構造体である。
  • st_tableは、さらにst_table_entryという構造体へのポインタを複数持っている。
  • st_table_entryは、以下の要素を持っている。
    • キーを変換したハッシュ、
    • キー、
    • 値、
    • 次のst_table_entryへのポインタ
  • キーを変換したハッシュは重複する可能性があるので...
    • ハッシュテーブルでヒットしたst_table_entryのキーを見て、重複した別のキーでないか確認する。
    • もし、重複している別のキーであれば、次のst_table_entryに移動して、一致するキーが見つかるまで繰り返す。
    • キーが一致していたら、値を読み出して返す。

つまり、キーが一致するまで、ポインタが指し示すst_table_entryへの移動を繰り返すことになる。キー値の検索はまずハッシュテーブルで行い、ハッシュが重複している場合はさらにif文で比較照合する、というハイブリッドなハッシュテーブルになっている。

f:id:zariganitosh:20090716103756j:image

図5: st_tableのデータ構造

st_tableに関連する構造体

  • 図5の構造体は、以下のように定義されている。
/* ---------- ruby_1_8_7/st.h ---------- */

#if SIZEOF_LONG == SIZEOF_VOIDP
typedef unsigned long st_data_t;
#elif SIZEOF_LONG_LONG == SIZEOF_VOIDP
typedef unsigned LONG_LONG st_data_t;
#else
# error ---->> st.c requires sizeof(void*) == sizeof(long) to be compiled. <<----
#endif

struct st_hash_type {
    int (*compare)();
    int (*hash)();
};

struct st_table {
    struct st_hash_type *type;
    int num_bins;
    int num_entries;
    struct st_table_entry **bins;
};
/* ---------- ruby_1_8_7/st.c ---------- */

struct st_table_entry {
    unsigned int hash;
    st_data_t key;
    st_data_t record;
    st_table_entry *next;
};

ポインタのポインタ

  • st_tableのstruct st_table_entry **binsは、ポインタマークが二つ付いている。
  • つまり、ポインタのポインタ、と言うことになる。何だか頭が混乱するが...
    • binsは配列を指していて、もしその配列に値が保存されていれば、値の型 *bins となるはずなのだが、
    • そこにはさらに、st_table_entryへのポインタがいくつも収められている。
    • だから、「st_table_entryへのポインタ型の、ポインタだよ。」と理解すれば、少しは分かり易いだろうか?
(st_table_entry*) (*bins)

  • ちなみに、binsは st_init_table_with_size() で取得されている。(以下の関数ポインタで抜粋したコード参照)
tbl->bins = (st_table_entry **)Calloc(size, sizeof(st_table_entry*));

関数ポインタ

  • ところで、st_tableの中に、st_hash_typeという(自分にとって)見慣れない構造体があることに気付く。
  • これは、関数を指し示すポインタの構造体である。
  • 関数ポインタであるcompareやhashに、特定の関数を指定すれば、その関数をポインタ識別子で呼び出すことができる。

例:

  • st_init_numtableを呼び出して生成されたst_tableでは...
/* ---------- ruby_1_8_7/st.c ---------- */

#ifdef RUBY
#define malloc xmalloc
#define calloc xcalloc
#endif

#define alloc(type) (type*)malloc((unsigned)sizeof(type))
#define Calloc(n,s) (char*)calloc((n),(s))

st_table*
st_init_numtable(void)
{
    return st_init_table(&type_numhash);
}

st_init_table(type)
    struct st_hash_type *type;
{
    return st_init_table_with_size(type, 0);
}

st_table*
st_init_table_with_size(type, size)
    struct st_hash_type *type;
    int size;
{
    st_table *tbl;
...(中略)...
    size = new_size(size);	/* round up to prime number */

    tbl = alloc(st_table);
    tbl->type = type;           /* tbl->type->compare = numcmp; tbl->type->hash = numhash; */
    tbl->num_entries = 0;
    tbl->num_bins = size;
    tbl->bins = (st_table_entry **)Calloc(size, sizeof(st_table_entry*));

    return tbl;
}

static struct st_hash_type type_numhash = {
    numcmp,
    numhash,
};

static int
numcmp(x, y)
    long x, y;
{
    return x != y;
}

static int
numhash(n)
    long n;
{
    return n;
}

  • st_tableのメンバ名を指定して、numcmp(x,y)あるいはnumhash(n)を呼び出すことができる。
(*table->type->compare)(x,y);  /* numcmp(x, y)と同等 */
(*table->type->hash)(n);       /* numhash(n)と同等 */

  • なぜ、わざわざ上記のような遠回しな関数呼び出しをするのか?
    • それは、st_hash_typeの指定を変更するだけで、ハッシュテーブルの種類に応じた比較(compare)やハッシュ変換(hash)を呼び出すことができるから。
  • ハッシュテーブルは、Rubyのいろいろな部分で活躍している。
  • しかし、利用する場所によって、ハッシュテーブルの種類が少しだけ異なる。
    • 上記のtype_numhashは、rubyのインタプリタでよく使われるタイプらしい。
    • Rubyコードの中で使うHashオブジェクトでは、objhashというタイプが使われている。

つまり、オブジェクト指向でないC言語で、オブジェクト指向的にハッシュテーブルを継承したような効果を狙っているのだ。コードの重複を最小限にするために。

Hashオブジェクトの実装

  • それでは、Rubyコードの中で利用する、タイプの違うHashオブジェクトの実装を追ってみた。
    • 一番下のrb_hash_new()が、RubyコードのHash.newに対応すると思われる。
# ---------- ruby_1_8_7/hash.c ----------

static int
rb_any_cmp(a, b)
    VALUE a, b;
{
    VALUE args[2];

    if (a == b) return 0;
    if (FIXNUM_P(a) && FIXNUM_P(b)) {
	return a != b;
    }
    if (TYPE(a) == T_STRING && RBASIC(a)->klass == rb_cString &&
	TYPE(b) == T_STRING && RBASIC(b)->klass == rb_cString) {
	return rb_str_cmp(a, b);
    }
    if (a == Qundef || b == Qundef) return -1;
    if (SYMBOL_P(a) && SYMBOL_P(b)) {
	return a != b;
    }

    args[0] = a;
    args[1] = b;
    return !rb_with_disable_interrupt(eql, (VALUE)args);
}

static int
rb_any_hash(a)
    VALUE a;
{
    VALUE hval;
    int hnum;

    switch (TYPE(a)) {
      case T_FIXNUM:
      case T_SYMBOL:
	hnum = (int)a;
	break;

      case T_STRING:
	hnum = rb_str_hash(a);
	break;

      default:
	hval = rb_funcall(a, id_hash, 0);
	if (!FIXNUM_P(hval)) {
	    hval = rb_funcall(hval, '%', 1, INT2FIX(536870923));
	}
	hnum = (int)FIX2LONG(hval);
    }
    hnum <<= 1;
    return RSHIFT(hnum, 1);
}

static struct st_hash_type objhash = {
    rb_any_cmp,
    rb_any_hash,
};

static VALUE
hash_alloc0(klass)
    VALUE klass;
{
    NEWOBJ(hash, struct RHash);
    OBJSETUP(hash, klass, T_HASH);

    hash->ifnone = Qnil;

    return (VALUE)hash;
}

static VALUE
hash_alloc(klass)
    VALUE klass;
{
    VALUE hash = hash_alloc0(klass);

    /* Hashオブジェクトが持つハッシュテーブルに、objhashタイプで初期化したハッシュテーブルを設定する */
    RHASH(hash)->tbl = st_init_table(&objhash);

    return hash;
}

VALUE
rb_hash_new()
{
    return hash_alloc(rb_cHash);/* rb_cHash = rb_define_class("Hash", rb_cObject); */
}
/* ---------- ruby_1_8_7/string.c ---------- */

int
rb_str_hash(str)
    VALUE str;
{
    register long len = RSTRING(str)->len;
    register char *p = RSTRING(str)->ptr;
    register int key = 0;

#if defined(HASH_ELFHASH)
    register unsigned int g;

    while (len--) {
	key = (key << 4) + *p++;
	if (g = key & 0xF0000000)
	    key ^= g >> 24;
	key &= ~g;
    }
#elif defined(HASH_PERL)
    while (len--) {
	key += *p++;
	key += (key << 10);
	key ^= (key >> 6);
    }
    key += (key << 3);
    key ^= (key >> 11);
    key += (key << 15);
#else
    while (len--) {
	key = key*65599 + *p;
	p++;
    }
    key = key + (key>>5);
#endif
    return key;
}
/* ---------- ruby_1_8_7/ruby.h ---------- */

struct RHash {
    struct RBasic basic;
    struct st_table *tbl;
    int iter_lev;
    VALUE ifnone;
};
  • Hashオブジェクトのキーには、数値・シンボル・文字列が利用できる。
  • st_tableのtypeがobjhashになると、以下のcompare・hashが呼び出されることになる。
(*table->type->compare)(x,y);  /* rb_any_cmp(x, y)と同等 */
(*table->type->hash)(n);       /* rb_any_hash(n)と同等 */
  • rb_any_cmp・rb_any_hashの中で、キーの種類に応じて処理されていた。

ハッシュの検索方法

  • まず、マクロ定義されたdo_hash(key,table)を実行することで、ハッシュ変換が始まる。......(4)
  • (*(table)->type->hash)( (key) )は、ポインタによる関数呼び出し。......(1)
    • ハッシュテーブルのタイプに応じたハッシュ変換関数が呼び出される。
  • FIND_ENTRY()はキーに対応するエントリを検索する......(5)
  • 求めたハッシュhash_valを、テーブルサイズ(table)->num_binsで割った余りが、求めるエントリの相対位置bin_posを示す。......(2)
  • ハッシュテーブルの先頭からbin_posをインデックス指定して、求めるエントリのポインタptrを取得する。......(3)
  • 取得したエントリのポインタptrが、果たして、検索キーと一致しているかチェックする。......(C)
    • PTR_NOT_EQUAL()とEQUAL()マクロによって、キーが一致していないとtrueを返す。......(B)
    • (*table->type->compare)( (x),(y) )でハッシュテーブルのタイプに応じた比較をする。......(A)
  • COLLISION;はデバッグ用のマクロなので、無視してOKらしい。
  • キーが一致していなければ、次のポインタに移動する。キーが一致するまで繰り返す。......(D)
    • 最後の ptr = ptr->next; が不要な気がするが、......(E)
    • if と whileのPTR_NOT_EQUALの第2引数が、ptrとptr->nextで違っていることに注意。
    • if を whileに置き換えれば、続く4行は不要になりそうだけど、COLLISIONを有効にしたいから、このようにしている?
  • 最後に、ptr->recordとすれば、キーに対応した値が取得できる。
/* ---------- ruby_1_8_7/st.c ---------- */

#define EQUAL(table,x,y) ((x)==(y) || (*table->type->compare)((x),(y)) == 0) /* A */

#define do_hash(key,table) (unsigned int)(*(table)->type->hash)((key))       /* 1 */

#define PTR_NOT_EQUAL(table, ptr, hash_val, key) \                           /* B */
((ptr) != 0 && (ptr->hash != (hash_val) || !EQUAL((table), (key), (ptr)->key)))

#define FIND_ENTRY(table, ptr, hash_val, bin_pos) do {\
    bin_pos = hash_val%(table)->num_bins;\           /* 2 */
    ptr = (table)->bins[bin_pos];\                   /* 3 */
    if (PTR_NOT_EQUAL(table, ptr, hash_val, key)) {\ /* C */
	COLLISION;\
	while (PTR_NOT_EQUAL(table, ptr->next, hash_val, key)) {\ /* D */
	    ptr = ptr->next;\
	}\
	ptr = ptr->next;\                                         /* E */
    }\
} while (0)

int
st_lookup(table, key, value)
    st_table *table;
    register st_data_t key;
    st_data_t *value;
{
    unsigned int hash_val, bin_pos;
    register st_table_entry *ptr;

    hash_val = do_hash(key, table);                  /* 4 */
    FIND_ENTRY(table, ptr, hash_val, bin_pos);       /* 5 */

    if (ptr == 0) {
	return 0;
    }
    else {
	if (value != 0)  *value = ptr->record;
	return 1;
    }
}

ハッシュテーブルのサイズ

  • ハッシュテーブルに必要なサイズは、st_init_table_with_size()の中で、new_size()を呼び出して計算している。
    • 配列primesに、2のn乗を超える素数を設定しておき、
    • ハッシュテーブルの要素数を収められる最も小さい素数を、配列primesから取得している。
/* ---------- ruby_1_8_7/st.c ---------- */

/*
 * MINSIZE is the minimum size of a dictionary.
 */

#define MINSIZE 8

/*
Table of prime numbers 2^n+a, 2<=n<=30.
*/
static long primes[] = {
	8 + 3,
	16 + 3,
	32 + 5,
	64 + 3,
	128 + 3,
	256 + 27,
	512 + 9,
	1024 + 9,
	2048 + 5,
	4096 + 3,
	8192 + 27,
	16384 + 43,
	32768 + 3,
	65536 + 45,
	131072 + 29,
	262144 + 3,
	524288 + 21,
	1048576 + 7,
	2097152 + 17,
	4194304 + 15,
	8388608 + 9,
	16777216 + 43,
	33554432 + 35,
	67108864 + 15,
	134217728 + 29,
	268435456 + 3,
	536870912 + 11,
	1073741824 + 85,
	0
};

static int
new_size(size)
    int size;
{
    int i;

#if 0
    /* 決して実行されないブロック */
    for (i=3; i<31; i++) {
	if ((1<<i) > size) return 1<<i;
    }
    return -1;
#else
    int newsize;

    for (i = 0, newsize = MINSIZE;
	 i < sizeof(primes)/sizeof(primes[0]);
	 i++, newsize <<= 1)
    {
	if (newsize > size) return primes[i];
    }
    /* Ran out of polynomials */
    return -1;			/* should raise exception */
#endif
}

テーブルサイズはなぜ素数なのか?

  • キーから値を取得するには、ハッシュ%テーブルサイズで余りを取得する。その余りが、求めるキーのインデックスになっている。
  • ハッシュテーブルを効率良く利用するには、インデックスの重複を無くし、キーと値が1対1となる関係を目指す必要がある。
  • それを実現するために、素数で割ることで、その余りが一様にばらけた値になるようだ。

ハッシュテーブルにエントリを追加

  • st_add_direct()は、エントリを無条件に追加する。
  • st_insert()は、ハッシュテーブルにキーが存在するか調べて...
    • キーが存在しない時だけ追加する。
    • キーが存在したら上書きする。
  • どちらも、追加する時は、マクロ定義 ADD_DIRECT()で処理している。
    • ハッシュテーブルが混雑していたら、......if (table->num_entries/(table->num_bins) > ST_DEFAULT_MAX_DENSITY)
    • ハッシュテーブルのサイズを再設定して、作り直す。......rehash(table)
    • st_table_entryに、必要事項を書き込んで、st_tableに追加する。st_tableのエントリ数を+1する。
#define ST_DEFAULT_MAX_DENSITY 5

#define ADD_DIRECT(table, key, value, hash_val, bin_pos)\
do {\
    st_table_entry *entry;\
    if (table->num_entries/(table->num_bins) > ST_DEFAULT_MAX_DENSITY) {\
	rehash(table);\
        bin_pos = hash_val % table->num_bins;\
    }\
    \
    entry = alloc(st_table_entry);\       /* 新規エントリを生成 */
    \
    entry->hash = hash_val;\
    entry->key = key;\
    entry->record = value;\
    entry->next = table->bins[bin_pos];\  /* 新規エントリのnext ← 今まで先頭だったエントリの位置 */
    table->bins[bin_pos] = entry;\        /* ハッシュテーブルが指す先頭のエントリ ← 新規エントリの位置 */
    table->num_entries++;\
} while (0)

int
st_insert(table, key, value)
    register st_table *table;
    register st_data_t key;
    st_data_t value;
{
    unsigned int hash_val, bin_pos;
    register st_table_entry *ptr;

    hash_val = do_hash(key, table);
    FIND_ENTRY(table, ptr, hash_val, bin_pos);

    if (ptr == 0) {
	ADD_DIRECT(table, key, value, hash_val, bin_pos);
	return 0;
    }
    else {
	ptr->record = value;
	return 1;
    }
}

void
st_add_direct(table, key, value)
    st_table *table;
    st_data_t key;
    st_data_t value;
{
    unsigned int hash_val, bin_pos;

    hash_val = do_hash(key, table);
    bin_pos = hash_val % table->num_bins;
    ADD_DIRECT(table, key, value, hash_val, bin_pos);
}

static void
rehash(table)
    register st_table *table;
{
    register st_table_entry *ptr, *next, **new_bins;
    int i, old_num_bins = table->num_bins, new_num_bins;
    unsigned int hash_val;

    new_num_bins = new_size(old_num_bins+1);
    new_bins = (st_table_entry**)Calloc(new_num_bins, sizeof(st_table_entry*));

    for(i = 0; i < old_num_bins; i++) {
	ptr = table->bins[i];
	while (ptr != 0) {
	    next = ptr->next;
	    hash_val = ptr->hash % new_num_bins;
	    ptr->next = new_bins[hash_val];
	    new_bins[hash_val] = ptr;
	    ptr = next;
	}
    }
    free(table->bins);
    table->num_bins = new_num_bins;
    table->bins = new_bins;
}

予備知識

C言語に疎いので、以下のようなことも、ちゃんと理解しておく必要があった。

#defineのマクロ機能
  • 自分が使ったことがある#defineは、以下のような使い方。
#define  PI  3.14

s = PI * r * r

  • すると、Cプリプロセッサは、以下のように置き換えて解釈してくれる。
s = 3.14 * r * r
  • つまり、#defineは、第1引数を、第2引数で置き換えるのだ。

  • 置き換えの機能をうまく利用すれば、以下のように関数定義のような使い方も可能。
#define  PI  3.14
#define  sq(r)  ((r)*(r))

s = PI * sq(r)
  • すると、以下のように解釈される。
s = 3.14 * ((r)*(r))

  • さらに、以下のようにすると、複数行に渡る関数定義のように利用できてしまうなんて!
#define FIND_ENTRY(table, ptr, hash_val, bin_pos) do {\
    bin_pos = hash_val%(table)->num_bins;\
    ptr = (table)->bins[bin_pos];\
    if (PTR_NOT_EQUAL(table, ptr, hash_val, key)) {\
	COLLISION;\
	while (PTR_NOT_EQUAL(table, ptr->next, hash_val, key)) {\
	    ptr = ptr->next;\
	}\
	ptr = ptr->next;\
    }\
} while (0)

FIND_ENTRY(table, ptr, hash_val, bin_pos);
  • すると、以下のように解釈されるのか...。
do {
    bin_pos = hash_val%(table)->num_bins;
    ptr = (table)->bins[bin_pos];
    if (PTR_NOT_EQUAL(table, ptr, hash_val, key)) {
	COLLISION;
	while (PTR_NOT_EQUAL(table, ptr->next, hash_val, key)) {
	    ptr = ptr->next;
	}
	ptr = ptr->next;
    }
} while (0);
  • また、while(0)の後に「;」を付けないのもポイントらしい。
    • マクロを利用する時は FIND_ENTRY(); とするので、while(0); だと「;」が二重になってしまう...。
構造体メンバへのアクセス
  • pがpoint型の変数であるなら、そのメンバには「.」で区切ってアクセスできる。
struct  point{
    float x;
    float y;
};

struct point p;
p.x = 100.0;
p.y = 200.0;
  • もし、pがポインタであるなら、「->」で区切ってアクセスする必要がある。
struct point *p;
p->x = 100.0;    /* (*p).xと同等 */
p->y = 200.0;    /* (*p).yと同等 */

追記


  • ハッシュオブジェクトの文字列キーから値へのインデックスを求める処理
/* ---------- ruby_1_8_7/string.c ---------- */

int
rb_str_hash(str)
    VALUE str;
{
    register long len = RSTRING(str)->len;
    register char *p = RSTRING(str)->ptr;
    register int key = 0;
...(中略)...
#else
    while (len--) {
	key = key*65599 + *p;
	p++;
    }
    key = key + (key>>5);
#endif
    return key;
}
  • その後、上記関数で返された値を、素数であるハッシュテーブルサイズで割った余りが、値へのインデックス
/* ---------- ruby_1_8_7/st.c ---------- */

#define FIND_ENTRY(table, ptr, hash_val, bin_pos) do {\
    bin_pos = hash_val%(table)->num_bins;
    ptr = (table)->bins[bin_pos];\
...(中略)...

  • 一方、st_hash_typeの場合の、値へのインデックスを求める処理
static int
numhash(n)
    long n;
{
    return n;
}
  • その後の処理は同様で、FIND_ENTRY(table, ptr, hash_val, bin_pos)が実行される。
  • ハッシュのタイプによっては、元のキーをそのままハッシュテーブルサイズで割って、余りを利用することがあるのだ。

素数で割るのは、汎用的に利用する為、なのかな?

2009-07-08

どのようにして一番右の1のビット位置を求めているのか?

「ものすごい」コードなのだけど、凄過ぎて自分には全くチンプンカンプン...。それでも、どの辺が凄いのか、ちゃんと理解したい。シンプルなコードから順を追って確かめてみた。

public static int GetNumberOfTrailingZeros( long x )
{
    if ( x == 0 ) return 64;

    ulong y = ( ulong ) ( x & -x );
    int i = ( int ) ( ( y * 0x03F566ED27179461UL ) >> 58 );
    return table[ i ];
}
static int[] table;

table = new int[ 64 ];
ulong hash = 0x03F566ED27179461UL;
for ( int i = 0; i < 64; i++ )
{
    table[ hash >> 58 ] = i;
    hash <<= 1;
}
一番右端の立っているビット位置を求める「ものすごい」コード - 当面C#と.NETな記録

rubyでシンプルに書く

  • 全く利用価値はないのだけど、理解を深める為に、上記のコードをRubyで書き直してみた。
  • 最も黒魔術的な 0x03F566ED27179461 をシフト操作する部分も、Rubyのハッシュを使えば省略できる。
# 一番右の1のビット位置を求める(1の右側に0がいくつあるか)
hash = 1
@table = {}
for i in (0..63)
  @table[hash] = i
  hash <<= 1
end

def trailing_zeros(x)
  return 64 if(x == 0)
    
  i = x & -x # 一番右の1のビットだけ残して、その他のビットは0にする
  @table[i]
end

p @table # @table => {1=>0, 2=>1, 4=>2, 8=>3, 16=>4, ... , 9223372036854775808=>63}
p trailing_zeros(8)  # 8.to_s(2)  => "1000" => 3
p trailing_zeros(10) # 10.to_s(2) => "1010" => 1

  • 3つの手順
    • @tableに2のn乗をキーに、1のビット位置を値とするハッシュを代入する。(2の63乗まで)
@table = {1=>0, 2=>1, 4=>2, 8=>3, 16=>4, ... , 9223372036854775808=>63}
    • 値xは、x & -x を計算することで、一番右の1を残して、その他のビットは全て0に変換される。
    • @tableハッシュで x & -x をキーにすれば、対応する値がビット位置になる。
  • 以上の3手順が主要な処理で、こうやってみると、分かり易い。
  • 答えをハッシュに保存して、キーで検索して、答えを求めているだけなのだ。

配列を使う時の問題

  • Rubyは相当人間寄りな言語で、ハッシュなんてものが使えるので、上記のようにシンプルに書ける。
  • もっとCPU寄りなC系言語では、ハッシュなんて存在しない。だから、配列を使う。
  • それでもRubyはずるくって、同様にシンプルに書けてしまうのだが、よく観察すると問題がある。
>> @table=[]
>> @table[1]=0
>> @table[2]=1
>> @table[4]=2
>> @table[8]=3
>> @table[16]=4
>> p @table
[nil, 0, 1, nil, 2, nil, nil, nil, 3, nil, nil, nil, nil, nil, nil, nil, 4]
  • 配列の利用状態が飛び飛びになっている。もし、@table[9223372036854775808]=63 なんてやってしまったら、相当な無駄遣い。
  • だから、配列は無駄なく64個にすることを考えなくてはならない。
table = new int[ 64 ];
  • そして、1、2、4、8, ... , 9223372036854775808というキー値を、0から63に変換して利用することになる。
  • どのように変換するか、実はその部分が黒魔術で、0x03F566ED27179461というビット列(M系列と呼ばれている)を活用している。

M系列とは?

  • 数式:Xn = Xn-p XOR Xn-qで表現される1ビットの周期的な数列
    • 例:p=3、q=1、初期値001の場合...
X3 = X3-3 XOR X3-1
   = X0 XOR X2
   = 0 XOR 1
   = 1
    • 上記のように順に計算していくと...
n012345678910111213
Xn00111010011101
    • 0011101を繰り返す

  • この数列をシフトしながら、3ビット(pビット)分ごとに取り出すと、0以外の全てのパターンを網羅すると言う不思議な特性がある。
    • 例:p=3、q=1、初期値001の場合...
00111010011101
00111010011101
00111010011101
00111010011101
00111010011101
00111010011101
00111010011101

2の累乗数列を順列に変換する

  • 8ビットの2の累乗数列1、2、4、8、16、32、64、128、をM系列を利用して、順列に変換してみる。
  • p=3、q=1、初期値001が繰り返すM系列:0b0011101を利用する
  • 000だけは繰り返しパターンから抜けてしまうが、先頭に0を追加して0b00011101とすることで、初回だけ000も取り出せる。
# 例:8を変換する場合
0b00011101 * 8 = 0b00011101000 # M系列に8をかけ算すると左に3ビットシフトする
0b00011101000 & 0xFF = 0b11101000 # 下位8ビットを取り出す
0b11101000 >> 5 = 0b111 # 右に5ビットシフトして、上位3ビットを取り出す。
  • つまり、0b00011101の太字の部分を取り出すことになる。
  • このようにして、8ビットの2の累乗数列を、重複の無い3ビットの順列に変換することができるのだ。
    • 例:p=3、q=1、初期値001の場合...
  1 => 000111010011101
  2 => 000111010011101
  4 => 000111010011101
  8 => 000111010011101
 16 => 000111010011101
 32 => 000111010011101
 64 => 000111010011101
128 => 000111010011101
    • 8ビットが、3ビットに圧縮されている!

変換テーブルを作って読み出す

  • 圧縮された3ビットは、0b000から0b111まで、つまり、0から7まで連続している。
  • よって、配列を使う時の問題で懸念された飛び飛びの利用状態が解消できることになる。
  • そして、以下のような配列を作っておいて、圧縮された3ビットをインデックスとして読み出せば、ビット位置が求められる。
インデックスビット位置
0x000(0)0
0x001(1)1
0x010(2)6
0x011(3)2
0x100(4)7
0x101(5)5
0x110(6)4
0x111(7)3

  • 上記は8ビットを3ビットに変換するM系列0b00011101(0x1D)だが、
  • 64ビットを6ビットに変換するのが、0x03F566ED27179461というM系列になるのだ。
  • 相変わらず実用的には無意味なRubyコードだが、理解のため書き直してみた。
# 一番右の1のビット位置を求める(1の右側に0がいくつあるか)
hash = 0x03F566ED27179461
@table = {}
for i in (0..63)
  @table[hash >> 58] = i
  hash <<= 1
  hash &= 0xFFFFFFFFFFFFFFFF # Rubyには符号無し64ビット型とか無いので強引に64ビットにした
end

def trailing_zeros(x)
  return 64 if(x == 0)
    
  y = x & -x # 一番右の1のビットだけ残して、その他のビットは0にする
  i = ((0x03F566ED27179461 * y) & 0xFFFFFFFFFFFFFFFF) >> 58
  @table[i]
end

p trailing_zeros(10)
p trailing_zeros(256)
p @table


# 実行結果...
#
# 10.to_s(2)  => "1010"
# trailing_zeros(10)  => 1
#
# 256.to_s(2) => "100000000"
# trailing_zeros(256) => 8
#
# @table => [0, 1, 59, 2, 60, 40, 54, 3, 61, 32, 49, 41, 55, 19, 35, 4, 62, 52, 30, 33, 50, 12, 14, 42, 56, 16, 27, 20, 36, 23, 44, 5, 63, 58, 39, 53, 31, 48, 18, 34, 51, 29, 11, 13, 15, 26, 22, 43, 57, 38, 47, 17, 28, 10, 25, 21, 37, 46, 9, 24, 45, 8, 7, 6]

結論

  • X & -Xを利用して、一番右の1のビットだけ残す。(賢い!)
  • 上記で取得できる64ビット2の累乗値を、M系列を利用して6ビットに圧縮する。(凄い!)
  • 配列を利用して、ビット位置を取得する。(高速!)

と、理解した。M系列を少しだけ理解できたことが最大の収穫!


あっ、こっちの方が分かり易いかも...。

2009-06-28

マルコフ連鎖で日本語をもっともらしく要約する

そもそも、マルコフ連鎖とは何なのか?全く聞いたこともなかった。そして、文章を要約するのはとっても高度なことだと思っていて、自分のレベルではその方法を、今まで思い付きもしなかった。

しかし、以下のようなシンプルなRubyコードでそれが出来てしまうと知った時、目から鱗である...。一体、何がどうなっているのだ?コードを追いながら、マルコフ連鎖を利用するという発想の素晴らしさを知った!

作業環境

  • MacBook OSX 10.5.7
  • ruby 1.8.6 (2008-08-11 patchlevel 287) [universal-darwin9.0]
  • mecab utf8環境でインストール済み

マルコフ連鎖に出逢う

rssを流し読みしていると、以下の日記に目が止まった。(素晴らしい情報に感謝です!)

一体何が出来るコードなのか、日記を読んだだけではピンと来なかったので、自分で実行してみることにした。コードは全掲載されているので、そのままコピーして、markov_chain.rbとして保存、ruby markov_chain.rbを実行してみる。が、1行目からエラーでまくり。まずは実行できる環境を整える必要があった。

実行環境を整える

  • MeCabを使ってマルコフ連鎖で説明されている通り、mecabのインストールを実行した。
  • ただし自分の場合、mecabについてはutf8環境でインストール済みなので、3行目のrb-mecabのみ実行した。
$ sudo port install mecab +utf8
$ sudo port install mecab-ipadic-utf8
$ sudo port install rb-mecab

  • これで require 'MeCab' が通ると思ったら、エラーでストップ。
  • どうやらrubyのロードパス($LOAD_PATHというグローバル変数)にrb-mecabの在処も含める必要があるようだ。(以下で、/opt/以下のパスが見当たらないので。)
$ ruby -e 'p $LOAD_PATH'
["/Library/Ruby/Site/1.8", "/Library/Ruby/Site/1.8/powerpc-darwin9.0", "/Library/Ruby/Site/1.8/universal-darwin9.0", "/Library/Ruby/Site", "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8", "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/powerpc-darwin9.0", "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/universal-darwin9.0", "."]

  • rubyの$LOAD_PATH(あるいは$:)は、シェルの環境変数$RUBYLIBを設定することで反映されるようだ。
  • そして、portでインストールしたrb-mecabは、/opt/local/lib/ruby/vendor_ruby/1.8/i686-darwin9/MeCab.bundleとして存在している。
  • よって、そのパス部分を環境変数$RUBYLIBに、以下のように代入した。
$ export RUBYLIB=/opt/local/lib/ruby/vendor_ruby/1.8/i686-darwin9

  • さらに、HTMLから必要な箇所を切り出してくれる'nokogiri'も、インストールが必要だった。
$ sudo gem install nokogiri
Password:
Building native extensions.  This could take a while...
Successfully installed nokogiri-1.3.2
1 gem installed
Installing ri documentation for nokogiri-1.3.2...
Installing RDoc documentation for nokogiri-1.3.2...

  • それから、ruby 1.8.6で each_cons を利用するには、require 'enumerator'が必要だった。
  • require 'enumerator'と、自分なりにコメントを追加したコードは、以下のようになった。
require 'MeCab'
require 'rubygems'
require 'nokogiri'
require 'open-uri'
require 'enumerator' # each_consを利用するため必要

# ヘッドラインの1行目の記事を取得する
url = 'http://www.asahi.com/'
text = String.new
nokogiri = Nokogiri::HTML.parse(open(url))
li = nokogiri.xpath('//div[@id="HeadLine"]/ul[@class="Lnk FstMod"]/li[1]/a')
nokogiri = Nokogiri::HTML.parse(open(url + li[0].attribute('href')))
nokogiri.xpath('//div[@class="BodyTxt"]/*').each do |body|
  text = text +  body.text
end
text.gsub!(/\n/,'')

# mecabで形態素解析して、 参照テーブルを作る
mecab = MeCab::Tagger.new("-Owakati")
data = Array.new
mecab.parse(text + "EOS").split(" ").each_cons(3) do |a| 
  data.push h = {'head' => a[0], 'middle' => a[1], 'end' => a[2]}
end

# マルコフ連鎖で要約
t1 = data[0]['head']
t2 = data[0]['middle']
new_text = t1 + t2  
while true
  _a = Array.new
  data.each do |hash|
    _a.push hash if hash['head'] == t1 && hash['middle'] == t2
  end 
 
  break if _a.size == 0
  num = rand(_a.size) # 乱数で次の文節を決定する
  new_text = new_text + _a[num]['end']
  break if _a[num]['end'] == "EOS"
  t1 = _a[num]['middle']
  t2 = _a[num]['end']
end

# EOSを削除して、結果出力
puts new_text.gsub!(/EOS$/,'')
http://d.hatena.ne.jp/sugarbabe335/20090613/1244858669

処理の流れ

  • mecabで形態素解析する。
    • スペース区切りで、品詞単位に分解
    • 例:「日本語を要約する」→「日本語 を 要約 する」
  • 最後に要約文を出力する。

要約の仕組み

上記において、マルコフ連鎖のみ利用したシンプルな要約とは、つまり、文章中の同じ文節を探して、乱数で選択しながら文章を繋げていく作業をしていたのだ。乱数頼りの機械的な作業だけども、偶然にもそれらしく、かなり的を得た射た要約になっていたりするから驚かされる。(乱数に左右されるので、実行する度に違った要約になる。)

$ ruby ~/Documents/markov_chain.rb
 【ロサンゼルス・タイムズ紙は市警当局者の話をもとに伝えている。市警は27日、ジャクソンさんが自宅で倒れた時に立ち会っていた担当医を参考人として事情聴取した。ハリウッドでは処方薬を飲んでいた」と明らかにしたが、死因の解明には4〜6週間かかる。ロサンゼルス郡検視局は「複数の処方薬を飲み過ぎで死亡する悲劇が相次ぐ。07年にモデルのアンナ・ニコル・スミスさんが複数の処方を頼まれた。詰問したら、以前に多くの医師から薬をもらっていたことを認めた」と米メディアに明かした。 26日、ロサンゼルス・タイムズ紙によると、検視局から遺体を引き渡された遺族は、死の真相を知るため、独自に病理学者に依頼して解剖をした。

$ ruby ~/Documents/markov_chain.rb
 【ロサンゼルス=堀内隆】歌手マイケル・ジャクソンさんの場合も友人の医師はラスベガスを本拠にする心臓内科医で、約2週間前からジャクソンさんが自宅で倒れた時に立ち会っていた担当医を参考人として事情聴取では死因につながる情報は出なかったと、ロサンゼルス・タイムズ紙は市警当局者の話をもとに伝えている。市警は27日、ジャクソンさんに与えた薬が死につながったとのうわさが飛び交っているからだ。 マリリン・モンローやエルビス・プレスリーら、米芸能界では処方薬の大量服用で命を落とす芸能人が後を絶たず、セレブと医師2人が起訴された。 南カリフォルニア大のジュリー・オルブライト講師はAP通信に「セレブの面倒を見ることで医師は大金を稼ぎ、彼らを薬漬けにする共犯になる」と危うい相互依存を指摘する。ジャクソンさんの急死した際は、大量の鎮痛剤を処方したとして医師は大金を稼ぎ、彼らを薬漬けにする共犯になる」と危うい相互依存を指摘する。ジャクソンさんの急死した際は、大量の鎮痛剤を処方したとして医師2人が起訴された。 南カリフォルニア大のジュリー・オルブライト講師は容疑者ではないと強調した。ジャクソンさんに与えた薬が死につながったとのうわさが飛び交っているからだ。 マリリン・モンローやエルビス・プレスリーら、米芸能界では処方薬を飲んでいた」と明らかにしたが、死因の解明には4〜6週間かかる。ロサンゼルス郡検視局から遺体を引き渡された遺族は、死の真相を知るため、独自に病理学者に依頼して解剖をした。

そして、日記の中ではうまく制御したり、学習させて重みを持たせると、面白いかもと言っている。その通り、本当に面白そうだ!

どのように学習させ、どのように重みを持たせるか、それが問題なのだけど。ベイジアンフィルターの仕組みなど、うまく利用できないだろうか...。(すぐには、良い使い方が閃かないけど)

追記

  • > もっともらしくて面白い。でも要約には全然なってない。スパムブログなら十分実用レベルかも知らないが
    • 全くその通りなのです。偶然にも要約されたような文章が出来上がるだけで、決して、係り受けとか、意味を解釈しての要約にはなっていないのだ...。
    • 本来の要約とは、自分の頭で理解して、自分の言葉で簡潔に表現することなので、まじめにそのような処理方法を考えると、自分のレベルでは行き詰まってしまう。
  • > s/的を得た/的を射た/
    • なるほど。すごく勉強になりました。今まで何の疑いも無く「的を得た」を使っていたのでした。
    • http://d.hatena.ne.jp/keyword/%C5%AA%A4%F2%C6%C0
    • それにしても自分は、54.3%(すでに過半数)の誤用している側の一人であったのだが...
    • さらに誤用率が上がれば、そう遠くない将来、「的を得た」も認める、という状況になってしまうかもしれない。
    • それは阻止するべきことなのか、それとも言葉の変化と捉えるべきなのか...
    • やはり、この日記を書いた時点での公式な正解、「的を射た」に修正することにしました。
    • s/的を得た/的を射た/についても、目から鱗(が落ちる)でした。ありがとうございました。

追記2

  • 今まで何の疑いも無く「的を得た」を使っていた身としては、救われた気がした。

「的を得た」も必ずしも間違いではなかった!

2009-05-12

sortableでin_placeなツリー操作まとめ

最近悩んでいたツリー操作のサンプル。自分用のメモ。ツリー構造の展開には、render(:partial)を利用するように変更してみた。(以前はヘルパメソッドを定義して展開していた。erbの方がツリー全体のイメージが理解し易いかも。)

環境

  • MacBook OSX 10.5.6
  • Rails 2.1.2

関連リンク

コマンドとコード

  • 初めの一歩。scaffold&プラグインのインストール。
# ---------- ターミナルでコマンド操作 ----------

rails _2.1.2_ test_tree
cd test_tree
script/generate scaffold tree name:string position:integer parent_id:integer
script/plugin install acts_as_tree
script/plugin install http://super-inplace-controls.googlecode.com/svn/trunk/super_inplace_controls

  • acts_as_treeを有効にする。
# ---------- app/models/tree.rb ----------

class Tree < ActiveRecord::Base
  acts_as_tree :order=>'position'
end

  • マイグレーションの中でTree.children.createして、サンプルレコードを追加している。
  • Tree.childrenメソッドを利用するため、先に上記Treeモデルでacts_as_treeを追記しておかないとエラーになる。
# ---------- db/migrate/20090507025853_create_trees.rb ----------

class CreateTrees < ActiveRecord::Migration
  def self.up
    create_table :trees do |t|
      t.string :name
      t.integer :position
      t.integer :parent_id

      t.timestamps
    end

    # root
    #   |--child
    #         |--sub_child
    #         |--sub_child
    root        = Tree.create(:name => 'root')
    child_1     = root.children.create(:name => 'child_1')
    sub_child_1 = child_1.children.create(:name => 'sub_child_1')
    sub_child_2 = child_1.children.create(:name => 'sub_child_2')
  end

  def self.down
    drop_table :trees
  end
end

  • DB作成とサーバー起動。
# ---------- ターミナルでコマンド操作 ----------

rake db:migrate
script/server

  • ルートの設定。:insert, :delete, :sortメソッドを追加した。
# ---------- config/routes.rb ----------

ActionController::Routing::Routes.draw do |map|
  map.resources :trees, :member => [:insert, :delete, :sort]
...(中略)...
  map.connect ':controller/:action/:id'
  map.connect ':controller/:action/:id.:format'
end

  • Tree.find(:all) は、 Tree.allとシンプルに書けるようだ。
  • insert、delete、sortアクションを定義。
# ---------- app/controllers/trees_controller.rb ----------

class TreesController < ApplicationController
  in_place_edit_for :tree, :name, :highlight_endcolor => "#444444"

  # GET /trees
  # GET /trees.xml
  def index
    @trees = Tree.all :conditions=>{:parent_id=>nil}, :order=>:position
    
    respond_to do |format|
      format.html # index.html.erb
      format.xml  { render :xml => @trees }
    end
  end
...(中略)...
  def insert
    @tree = Tree.find(params[:id]).children.create(:name=>'untitled')
    
    if @tree
      render :update do |page|
        page.insert_html(:top, dom_id(@tree.parent, :parent), render(:partial=>@tree))
        page.visual_effect(:highlight, dom_id(@tree), :duration=>0.6)
        page.replace_html(:info, "Tree was successfully created.")
        # 新規追加したアイテムをドラッグ可能にするため
        page.replace_html('sortable_script', sortable_element_tree)
      end
    else
      render :update do |page|
        page.replace_html(:info, "Tree failed to insert child.")
      end
    end
  end

  def delete
    @tree = Tree.find(params[:id]).destroy
    
    render :update do |page|
      page.visual_effect(:highlight, dom_id(@tree), :duration=>0.6)
      page.delay(0.8) do
        page.visual_effect(:drop_out, dom_id(@tree))
      end
      page.delay(1.6) do
        page.remove(dom_id(@tree))
        page.replace_html(:info, "Tree was successfully deleted.")
      end
    end
  end

  def sort
    # 再描画の必要がなければ、render :nothing => true
    # paramsの内容を確認したいため、render :text => params.inspect
    # sortable_elementが送信するparamsの内訳を正確に表示するには、renderを最初に実行する必要あり
    render :text => params.inspect
    save_tree(params[:tree_], nil)
  end

private
  # ツリー構造("tree_" => )は、以下のハッシュが再帰的に繰り返されて、表現される
  #   {ポジション番号=>{id=>番号, 子ポジション番号=>{id=>番号, 孫ポジション番号=>{id=>番号, ...} } } }
  #   子を持たない世代(ポジション番号=>{id=>番号})まで繰り返される
  # 例:
  #   root(1)
  #     |--child(2)
  #          |--sub_child(3)
  #          |--sub_child(4)
  #                   |--sub_sub_child(5)
  #   "tree_" => {"0"=>{"id"=>"1", "0"=>{"id"=>"2", "0"=>{"id"=>"3"}, "1"=>{"id"=>"4", "0"=>{"id"=>5}}}}}
  #
  # ツリー構造全体を保存する
  def save_tree(tree, parent)
    tree.each do |order, hash|
      id = hash.delete(:id)
      item = Tree.find(id)
      item.update_attributes(:position=>order, :parent_id=>parent)
      save_tree(hash, id) unless hash.empty?
    end    
  end
end

  • index.html.erbで、最初のulタグのみ設定して...
  • _tree.html.erbで、<li>自分の内容<ul>子のリスト</ul></li>を再帰的に繰り返す。
<%# ---------- app/views/trees/index.html.erb ---------- %>

<h1>Listing trees</h1>

<ul class="sortable_tree" id="tree_">
  <%= render :partial => 'tree', :collection => @trees %>
</ul>

<div id="sortable_script">
  <%= sortable_element_tree %>
</div>

<div id="info"></div>
<%# ---------- app/views/trees/_tree.html.erb ---------- %>

<% content_tag_for :li, tree do %>
  <%= toggle_link_for(tree, :parent, :default_display=>tree.children.empty?) %>
  <%= in_place_text_field(:tree, :name, :object=>tree, :endcolor=>"#444444", :restorecolor=>"#444444") %>
  <%= link_to_remote("子追加", {:url=>insert_tree_path(tree)}, :class=>'action') %>
  <%= link_to_remote("削除", {:url=>delete_tree_path(tree), :confirm=>'Are you sure?'}, :class=>'action') %>
  <%= content_tag(:span, "↑↓", :class=>'handle') %>  
  
  <% content_tag_for(:ul, tree, :parent, :style=>("display:none" unless tree.children.empty?)) do %>
    <%= content_tag(:li) %>
    <%= render(:partial=>'tree', :collection=>tree.children) unless tree.children.empty? %>
  <% end %>
<% end %>

f:id:zariganitosh:20090508115102p:image


  • toggle_link_forは、ツリーを開閉するリンク。
  • sortable_element_treeは、子アイテムを新規追加する度に呼び出される。(新規アイテムをドラッグ可能にするため必要だった。)
# ---------- app/helpers/trees_helper.rb ----------

module TreesHelper
  # 子ツリーを開閉する矢印リンクを作成
  def toggle_link_for(record, *args)
    prefix  = args.first.is_a?(Hash) ? nil : args.shift
    options = args.extract_options!
    options[:default_display] ? collapse_link_display = "display:none" : expanded_link_display = "display:none"
    options[:expanded_mark] ||= "&nbsp;∨&nbsp"
    options[:collapse_mark] ||= "&nbsp;≫&nbsp"
    
    link_to_function(options[:expanded_mark], 
      update_page do |page|
        page.visual_effect(:slide_up, dom_id(record, prefix), :duration=>0.2)
        page.toggle(dom_id(record, 'expanded'), dom_id(record, 'collapse'))
      end, 
      :class=>dom_class(record, 'expanded'), :id=>dom_id(record, 'expanded'), :style=>expanded_link_display ) + 
    
    link_to_function(options[:collapse_mark], 
      update_page do |page|
        page.visual_effect(:slide_down, dom_id(record, prefix), :duration=>0.2)
        page.toggle(dom_id(record, 'expanded'), dom_id(record, 'collapse'))
      end, 
      :class=>dom_class(record, 'collapse'), :id=>dom_id(record, 'collapse'), :style=>collapse_link_display )
  end

  # indexアクションで、ツリー構造をドラッグ可能にする
  # insertアクションで、新規追加アイテムもドラッグ可能にする
  def sortable_element_tree
    sortable_element('tree_', 
                     :update=>'info', 
                     :url=>{:action=>'sort'}, 
                     :handle=>'handle', 
                     :tree=>true)
  end
end

  • Railsやプラグインの既存の仕様に満足できないところをコピーして修正している。(オレンジ色の部分)
# ---------- app/helpers/application_helper.rb ----------

# Methods added to this helper will be available to all templates in the application.
module ApplicationHelper
end

# visual_effectのオプション指定で、クォート込みの"'文字列'"、クォート無しの"文字列"、どちらの指定も可能にするための修正
# 例:
#   visual_effect(:highlight, id_string, :endcolor=>"'#ffffff'")
#   visual_effect(:highlight, id_string, :endcolor=>"#ffffff")
module ActionView
  module Helpers
    module ScriptaculousHelper
      def visual_effect(name, element_id = false, js_options = {})
        element = element_id ? element_id.to_json : "element"
        
        js_options[:queue] = if js_options[:queue].is_a?(Hash)
          '{' + js_options[:queue].map {|k, v| k == :limit ? "#{k}:#{v}" : "#{k}:'#{v}'" }.join(',') + '}'
        elsif js_options[:queue]
          "'#{js_options[:queue]}'"
        end if js_options[:queue]
        
        [:endcolor, :direction, :startcolor, :scaleMode, :restorecolor].each do |option|
          js_options[option] = "'#{js_options[option]}'" if js_options[option] && !(/\A(['"]).+\1\z/ =~ js_options[option])
        end

        if TOGGLE_EFFECTS.include? name.to_sym
          "Effect.toggle(#{element},'#{name.to_s.gsub(/^toggle_/,'')}',#{options_for_javascript(js_options)});"
        else
          "new Effect.#{name.to_s.camelize}(#{element},#{options_for_javascript(js_options)});"
        end
      end
    end
  end
end

# super_inplace_controlの修正
module Flvorful  
  module SuperInplaceControls
    module HelperMethods
      protected
      # :startcolor, :endcolor, :restorecolorオプション設定を追加
      def in_place_field(field_type, object, method, options)
        object_name = object.to_s
        method_name = method.to_s
        @object = self.instance_variable_get("@#{object}") || options[:object] 
        display_text = set_display_text(@object, method_name, options)
        ret =  html_for_inplace_display(object_name, method_name, @object, display_text, options)
        ret << form_for_inplace_display(object_name, method_name, field_type, @object, options)
      end
      
      def html_for_inplace_display(object_name, method_name, object, display_text, opts)
        options = {}
        options.merge!(:startcolor => opts.delete(:startcolor)) if opts[:startcolor]
        options.merge!(:endcolor => opts.delete(:endcolor)) if opts[:endcolor]
        options.merge!(:restorecolor => opts.delete(:restorecolor)) if opts[:restorecolor]
        
        id_string = id_string_for(object_name, method_name, object)
        content_tag(:span, display_text, 
                    :onclick => update_page do |page|
                      page.hide "#{id_string}"
                      page.show "#{id_string }_form"
                    end, 
                    :onmouseover => visual_effect(:highlight, id_string, options), 
                    :title => "Click to Edit", 
                    :id => id_string ,
                    :class => "inplace_span #{"empty_inplace" if display_text.blank?}" 
                    )
      end

      # 最終行の改行タグを削除
      def form_for_inplace_display(object_name, method_name, input_type, object, opts)
        retval = ""
        id_string = id_string_for(object_name, method_name, object)
        set_method = opts[:action] || "set_#{object_name}_#{method_name}"
        save_button_text = opts[:save_button_text] || "OK"
        loader_message = opts[:saving_text] || "Saving..."
        retval << form_remote_tag(:url => { :action => set_method, :id => object.id },
        :method => opts[:http_method] || :post,
        :loading => update_page do |page|
          page.show "loader_#{id_string}"
          page.hide "#{id_string}_form"
        end,
        :complete => update_page do |page|
          page.hide "loader_#{id_string}"
        end,
        :html => {:class => "in_place_editor_form", :id => "#{id_string}_form", :style => "display:none" } )

        retval << field_for_inplace_editing(object_name, method_name, object, opts, input_type )
        retval << content_tag(:br) if opts[:br]
        retval << submit_tag( save_button_text, :class => "inplace_submit")
        retval << link_to_function( "Cancel", update_page do |page|
          page.show "#{id_string}"
          page.hide "#{id_string}_form"
        end, {:class => "inplace_cancel" })
        retval << "</form>"
        retval << invisible_loader( loader_message, "loader_#{id_string}", "inplace_loader")
        #retval << content_tag(:br)
      end
    end
  end
end

  • スタイルシートの設定。アイテムの1行表示やマウスオーバーの挙動。
  • 子無しアイテムにドラッグするには、liのpaddingに上下幅が必要。
  • 下のpaddingを多めにした方が自然にドラッグできるような気がした。
/* ---------- public/stylesheets/tree.css ---------- */

ul.sortable_tree, 
.sortable_tree ul {
  list-style-type: none;
  list-style-position: inside;
  padding: 0px;
  margin: 0 0 0 2em;
}

.sortable_tree li {
  text-decoration: underline;
  padding: 0 0px 6px 0px;
  margin: 0;
}

a.expanded_tree, 
a.collapse_tree, 
a.action {
  text-decoration: none;
  font-size: 66%;
  color: #000;
  background-color: #ccc;
}

a.expanded_tree:hover, 
a.collapse_tree:hover, 
.handle:hover,
a.action:hover {
  color: #fff;
  background-color: #000;
}

.handle {
  text-decoration: none;
  font-size: 100%;
  cursor: move;
}

.in_place_editor_field, 
.inplace_span {
  text-decoration: none;
  font-size: 100%;
  color: #eee;
  background-color: #444;
}

div.inplace_loader,
form.in_place_editor_form,
form.in_place_editor_form * {
    display: inline;
}

  • 忘れずに書いておかないと、動かなくて悩む。
    • stylesheet_link_tag 'tree'
    • javascript_include_tag :defaults
<%# ---------- app/views/layouts/trees.html.erb ---------- %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
       "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
  <title>Trees: <%= controller.action_name %></title>
  <%= stylesheet_link_tag 'tree', 'scaffold' %>
  <%= javascript_include_tag :defaults %>
</head>
<body>

<p style="color: green"><%= flash[:notice] %></p>

<%= yield  %>

</body>
</html>

参考ページ

以下のページを見て、最もシンプルなツリー操作の方法が理解できました。感謝です!


サンプルデモ

オプションデモ

2009-05-11

自分好みなin_place_editor(on super_inplace_control)を目指して

ツリー操作のサンプルの中で、in_place_editorを弄っていると、だんだん些細なことが気になってきた。

  1. ハイライト中にマウスオーバーしてさらにハイライト処理が実行されると、背景色が変化途中の黄色で終わってしまう...。
  2. テキスト編集する時に、フォームの後ろで改行されてしまう。常に1行で表示させたい。
  3. テキスト編集後のハイライトが、黄色→白→黒と変化するのが見苦しい。黄色→黒で変化して欲しい。
  4. テキスト保存時のプログレスイージケーターが、一瞬だけど、改行されてしまうのが見苦しい。

以下、自分好みを実現しようとして、理解できたこといろいろメモ。

in_place_editor_fieldが生成する基本のerb

  • super_inplace_controlの機能はRailsのヘルパメソッドを組み合わせて実現されていることに気付いた。(script.aculo.usのAjax.InPlaceEditorには頼らずに)
  • 多少は使い慣れているRubyコードで書かれていると、(自分にとっては)生javascriptを読むより遥かに理解し易い。
  • in_place_editor_field(:tree, :name) で生成されるコードは、- 仮にTreeモデルで、:idが100、:nameが’root’のin_placeなアイテムなら - 以下のerbで表現される。

f:id:zariganitosh:20090511094515p:image

      • rootをクリックして編集モードへ

f:id:zariganitosh:20090511094159p:image

<%= content_tag(:span, 'root', 
                :onclick => update_page do |page|
                  page.hide "tree_name_100"
                  page.show "tree_name_100_form"
                end, 
                :onmouseover => visual_effect(:highlight, 'tree_name_100'), 
                :title => "Click to Edit", 
                :id => "tree_name_100" ,
                :class => "inplace_span" ) %>

<%= form_remote_tag(:url => { :action => 'set_tree_name_100', :id => 1 },
                    :method => :post,
                    :loading => update_page do |page|
                      page.show "loader_tree_name_100"
                      page.hide "tree_name_100_form"
                    end,
                    :complete => update_page do |page|
                      page.hide "loader_tree_name_100"
                    end,
                    :html => {:class => "in_place_editor_form", :id => "tree_name_100_form", :style => "display:none" } ) %>

  <%= text_field(:tree, :name) %>
  <%= submit_tag('OK', :class => "inplace_submit") %>
  <%= link_to_function("Cancel", 
                       update_page do |page|
                         page.show "tree_name_100"
                         page.hide "tree_name_100_form"
                       end, 
                       {:class => "inplace_cancel" } ) %>

</form>

<% content_tag(:div, :id => 'loader_tree_name_100', :class => 'inplace_loader', :style => "display:none") do %>
  <%= image_tag("spinner.gif") + "&nbsp;&nbsp;" + content_tag(:span, 'Saving...') %>
<% end %>
  • 試しに上記コードをコピーして、ビューに貼付けてみると、in_placeな動作が確認できた。
  • super_inplace_controlのコードの方は、様々なオプション設定に対応するためもう少し複雑だが...
  • 基本の雛形が理解できれば、あとは見慣れたerbなので、いくらでも自由にカスタマイズできそう。

super_inplace_controls.rbの修正

  • 課題:1. ハイライト中にマウスオーバーしてさらにハイライト処理が実行されると、背景色が変化途中の黄色で終わってしまう...。
    • ハイライト変化途中の黄色が、endcolor、restorecolorとして記憶されてしまうことが原因のようだ。
    • super_inplace_controls.rbから、修正したいメソッドだけ選択して、application_helper.rbにコピーした。
    • ハイライトのendcolor、restorecolorをオプション指定できるようにコードを修正した。(オレンジ色の部分が修正箇所)
  • 課題:2. テキスト編集する時に、フォームの後ろで改行されてしまう。常に1行で表示させたい。
    • 不要な改行コードを生成している行をコメントアウト。(retval << content_tag(:br)の部分)
# ---------- app/helpers/application_helper.rb ----------
module ApplicationHelper
...(中略)...
end
...(中略)...
module Flvorful  
  module SuperInplaceControls
    module HelperMethods
      protected  
      def in_place_field(field_type, object, method, options)
        object_name = object.to_s
        method_name = method.to_s
        @object = self.instance_variable_get("@#{object}") || options[:object] 
        display_text = set_display_text(@object, method_name, options)
        ret =  html_for_inplace_display(object_name, method_name, @object, display_text, options)
        ret << form_for_inplace_display(object_name, method_name, field_type, @object, options)
      end
      
      def html_for_inplace_display(object_name, method_name, object, display_text, opts)
        options = {}
        options.merge!(:startcolor => opts.delete(:startcolor)) if opts[:startcolor]
        options.merge!(:endcolor => opts.delete(:endcolor)) if opts[:endcolor]
        options.merge!(:restorecolor => opts.delete(:restorecolor)) if opts[:restorecolor]
        
        id_string = id_string_for(object_name, method_name, object)
        content_tag(:span, display_text, 
                    :onclick => update_page do |page|
                      page.hide "#{id_string}"
                      page.show "#{id_string }_form"
                    end, 
                    :onmouseover => visual_effect(:highlight, id_string, options), 
                    :title => "Click to Edit", 
                    :id => id_string ,
                    :class => "inplace_span #{"empty_inplace" if display_text.blank?}" 
                    )
      end

      def form_for_inplace_display(object_name, method_name, input_type, object, opts)
        retval = ""
        id_string = id_string_for(object_name, method_name, object)
        set_method = opts[:action] || "set_#{object_name}_#{method_name}"
        save_button_text = opts[:save_button_text] || "OK"
        loader_message = opts[:saving_text] || "Saving..."
        retval << form_remote_tag(:url => { :action => set_method, :id => object.id },
        :method => opts[:http_method] || :post,
        :loading => update_page do |page|
          page.show "loader_#{id_string}"
          page.hide "#{id_string}_form"
        end,
        :complete => update_page do |page|
          page.hide "loader_#{id_string}"
        end,
        :html => {:class => "in_place_editor_form", :id => "#{id_string}_form", :style => "display:none" } )

        retval << field_for_inplace_editing(object_name, method_name, object, opts, input_type )
        retval << content_tag(:br) if opts[:br]
        retval << submit_tag( save_button_text, :class => "inplace_submit")
        retval << link_to_function( "Cancel", update_page do |page|
          page.show "#{id_string}"
          page.hide "#{id_string}_form"
        end, {:class => "inplace_cancel" })
        retval << ""
        retval << invisible_loader( loader_message, "loader_#{id_string}", "inplace_loader")
        #retval << content_tag(:br)
      end
    end
  end
end

in_place_edit_forのオプション指定

  • 課題:3. テキスト編集後のハイライトが、黄色→白→黒と変化するのが見苦しい。黄色→黒で変化して欲しい。
    • in_place_edit_forのオプション指定で対応できた。
class TreesController < ApplicationController
  in_place_edit_for :tree, :name, :highlight_endcolor => "#444444"
...(中略)...

cssの設定

  • 課題:2. テキスト編集する時に、フォームの後ろで改行されてしまう。常に1行で表示させたい。
  • 課題:4. テキスト保存時のプログレスイージケーターが、一瞬だけど、改行されてしまうのが見苦しい。
    • テキスト編集時のフォームをすべてinlineに。
    • プログレスイージケーターもinlineに。
div.inplace_loader,
form.in_place_editor_form,
form.in_place_editor_form * {
    display: inline;
}

所感

  • super_inplace_controlはテキスト以外にも、チェックボックス、ラジオボタン、セレクトリスト、カレンダー等も利用できて、名前の通りホントにsuper。
  • in_place_editorは派手なGUIで毎回興味をそそられるのだが、実は最初からフォームを表示しておいて、Ajaxな更新をするだけでも十分なのではないか...。
  • そう思ったら、最初の「in_place_editor_fieldが生成する基本のerb」を雛形に、自分でヘルパメソッドを定義すれば良いのかもしれない。