Rubyの多重代入と可変長引数

Rubyの多重代入・可変長引数はとても便利だが、トリッキーに使ってしまうとコードが追いにくくなる。リファレンスマニュアルには下記のように例が示されている。

 foo, bar = [1, 2]       # foo = 1; bar = 2
 foo, bar = 1, 2         # foo = 1; bar = 2
 foo, bar = 1            # foo = 1; bar = nil

 foo, bar, baz = 1, 2    # foo = 1; bar = 2; baz = nil
 foo, bar = 1, 2, 3      # foo = 1; bar = 2
 foo      = 1, 2, 3      # foo = [1, 2, 3]
 *foo     = 1, 2, 3      # foo = [1, 2, 3]
 foo,*bar = 1, 2, 3      # foo = 1; bar = [2, 3]

基本的には問題ないと思うが、「foo = 1, 2, 3」を配列と解釈するのはどうなのかと思ってしまう。明示的に「*」を付けない限りは一つ目の値を入れてくれた方が(個人的に)しっくりくる。

例えば下記のようなコードを書く。はい、ただのTypoですすみません。

a = 1,
b = 2

その変数の中身を出力する。

p a
  => [1, 2]
p b
  => 2

そりゃないよ、という解釈をされている。

acts_as_taggable_alternate

Railsで「タグ」を実装する場合には「acts_as_taggable」を利用するのがスタンダードと思われるが、実際に使用してみたところ結構不満点がでてきた。

  1. find_tagged_with
    1. なんでAR#findの各オプションが使えないの?
      • 「:include」「:order」「:conditions」あたりのオプションは使いたくなることが多いと思う。特にARモデルのリスト表示をする際に「:include => :tags」としたいことは多いんじゃないかなぁ。
    2. 複数のタグを指定するとどうしてOR条件で検索されるの?
      • タグで検索する場合って複数のタグを指定したら絞り込みを期待するのが普通なんじゃないのかな?
  2. ページネーションをするにはSQL文書かないと駄目?
    • 自分の力不足なのかもしれないけれどcount取得するところでどうしてもfind_by_sqlを実行しないと取れない。ライブラリ側でラップしてくれればいいのになぁ。
  3. 出来れば全角スペースも裏でよきにはからって欲しいなぁ
    • マルチバイト圏の人にしかわからぬ悩みだよね。\sに全角スペースが仲間入りするのが一番嬉しいんだけど。

ActsAsTaggableHowToにカスタマイズ方法や他の人が拡張したPluginも紹介されていたけど、しっくりくるものが無かったのでalternate版を作ってみた。

特徴:

  1. acts_as_taggable (Rails Plugin版)の機能は全てそのまま使える
  2. AR#findのOptionが(多分)全て使える
  3. ページネーションが簡単に実現できる
  4. タグクラウドが簡単に出力できる

ZIP:http://rubyforge.org/projects/rubricks/
SVNsvn://dev.rubricks.org/var/svn/rubricks/plugins/acts_as_taggable_alternate/trunk/acts_as_taggable_alternate
APIhttp://dev.rubricks.org/wiki/RubricksDevelopmentActsAsTaggableAlternateJa


includeやpaginateの処理に関してはSQL文を書いているので、様々な実行環境に対応できているとは言い難い。使ってくれる奇特な方がいれば動作報告をもらえると嬉しいです。


(2007/07/12 追記)
Rubricksプラグインを自前のacts_as_taggableに変えた途端、色々と要望がきたので頻出タグを抽出するメソッドとタグクラウドを出力するヘルパを追加。


(2007/07/13 追記)
3日間で4リリース。ちゃんと使い込んでから出さないと駄目だな。コミットログをみながら恥ずかしさに悶絶。どこにも宣伝していないのだが既に数人ダウンロードして頂いた方がいらっしゃるようだ。ごめんなさい、最新版に差し替えて下さい。


(2007/07/20 追記)
「acts_as_taggable」だと本家と紛らわしいとのご指摘を頂いたのでプラグイン名を変更した。


(2007/07/26 追記)
ドキュメントをちゃんと書いたので、リンクを貼って当記事からは削除したらやけにすっきりした。気が向いたら利用例でも書こうか。

Rubricks on Rails on JRuby

OSS関係団体の視察で米国Sun Microsystems本社を訪問、Mr.Charles NutterからJRubyの紹介を聞いて感銘を受けたのが2月頃。
帰国してすぐにJRuby0.9.xでRubricksを動かそうとしたけどウンともスンとも言わなかった。


そんなRubricksが、JRuby1.0上で動きましたよ!時々エラー出たりするけど、動いてる動いてる。


期待してたパフォーマンスは!残念、ちょっと遅くなってます。体感的には1.5〜2倍位かなぁ。今後に期待。がんばれCharles!

(Shouta)

Rails/Ajax高速化関係メモ

RubyKaigiでも発表させてもらったのですが、Rubricks0.6リリースに向けてパフォーマンスをがんばって上げております。
以下、つらつらと。

render_componentが遅い

  • render_componentはrequest.dupしてコントローラに投げなおすようなつくりになってて無駄が多い。
  • シンプルなsimple_render_componentを自作して解決。

→コントローラの処理時間がに5倍近く高速化

IEはDOM操作を行うと重い

  • 一般的にDOM操作よりもinnerHTMLの方が速い。
  • SpinelzをDOM操作ではなく極力innerHTMLで操作するように改良

IEで約3倍の高速化

毎回JSライブラリを読み直しは重い

  • 数百kbyteのJSライブラリを読み直すのに時間がかかっている。
  • ほとんどのJSをログイン時に先読みし、画面更新を全てAjaxベースで実施するように改良。
    • ※ハッシュを駆使して戻るボタンはきくようにした。

→約2倍の高速化(evalScriptsが少ないページ)

IEでevalScripts:trueのAjaxが重い

  • 上記対処をしたところ、先読み化しきれないJSが多く含まれるページが元よりも重くなった。(IEのみ)
  • Ajaxを用いてscriptを評価するにはevalを用いるため、IEのevalが遅いのが原因。
    • 普通にJSを読み込む場合の倍近く時間がかかる。
  • 基本的には対処できないが、空白や改行などをカットしてeval対象文字列を小さくして緩和。

→2倍程度の高速化(evalScriptsが多いページ)

IEを長時間使っていると重くなってくる

  • よく話題になるメモリリークが原因。
  • 擬似画面遷移時にprototype.jsのEvent.unloadCache();を呼んで解放することで大抵は解決。
    • (その際にDraggables.drags = [];をしないとDnDが一切効かなくなる)
  • 普通に画面遷移するアプリなら画面遷移時に自動でunloadされるので意識する必要はない。
  • ただし、2007/6/15のWindowsUpdateに含まれていたパッチでリーク自体ほとんどしなくなった。

(Shouta)

textfieldの個数によってformがsubmitされたりされなかったりする

フォームにフォーカス当てて、Enterを押すとサブミットされる時とされない時があるのに気づいたことはありますか?
衝撃的な事にフォームに含まれるテキストフィールドの個数で挙動が変わるのです。

例えば

<form>
  <input type='text' />
</form>

の場合、テキストフィールドにフォーカスを合わせてEnterを押すとonsubmit処理が実施されて画面遷移します。

一方

<form>
  <input type='text' />
  <input type='text' />
</form>

の場合、Enterを押しても無反応です。

ところが、下記のようにサブミットボタンを追加してEnterを押すと今度は画面遷移します。

<form>
  <input type='text' />
  <input type='text' />
  <input type='submit' />
</form>

更にこのサブミットボタンのスタイルをdisplay:none;にしたり、disabledにしたりするとまた挙動が変わります。
ブラウザによっても微妙に動きが異なります。

今のところ調査した感じだと下記のような挙動のようです。

○・・・Enterで画面遷移する
×・・・Enterを押しても無反応

テキストフィールド1つの場合

IE FF
常に○ 常に○

テキストフィールド2つの場合

  IE FF
サブミットボタンのdisplay:none ×
サブミットボタンがdisabled × ×
サブミットボタンがない × ×

それにしても、なぜこんな仕様になってるんでしょうか。何か歴史的な問題?
ちなみに、Rubricksではこの問題にぶち当たった箇所で挙動を統一化するためにヘルパでdisplay:noneなテキストフィールドを2つ強制的に作成するようにしました。

http://dev.rubricks.org/wiki/RubricksDevelopmentTipsFormSubmitJa

(Shouta)

Railsのpaginateでdistinctを扱う

ActiveRecordでdistinctを伴うpaginateをやろうとするとなかなかうまく行きません。
いろいろいじくってやり方を編み出したのでメモ。

まず、ActiveRecordでfindのdistinctを扱うには下記のようにします。

>> RubricksUser.find(:all, :select => 'distinct *')
SELECT distinct * FROM rubricks_users

また、countのdistinctを扱うには下記のようにします。

>> RubricksUser.count(:distinct => true, :select => 'id')
SELECT count(DISTINCT id) AS count_id FROM rubricks_users

さて、paginateでdistinctを使うにはどうすればいいか。
今回やった方法は下記のような感じです。

@pages, @user_list = paginate(:rubricks_user,
  :per_page => 5,
  :select => "distinct *",
  :distinct => true,
  :count    => 'id'
)

paginateでは検索結果を得るためのfindとページ番号を算出するためのcountが両方発行されます。そのため、双方のオプションを重ね合わせるようにパラメータを渡す必要があります。
つまり、:selectはAR.findへ、:distinctはAR.countへ渡されます。なお、:countオプションというのはAR.countの:selectオプションに渡されるものです。

ただし、この方法はそのままでは通らず、エラーが出ます。
paginateの中で
(1):distinctオプションがなぜか不正としてはじかれてしまう

#pagination.rb (distinctがない)
      DEFAULT_OPTIONS = {
        :class_name => nil,
        :singular_name => nil,
        :per_page   => 10,
        :conditions => nil,
        :order_by   => nil,
        :order      => nil,
        :join       => nil,
        :joins      => nil,
        :count      => nil,
        :include    => nil,
        :select     => nil,
        :parameter  => 'page'
      }
...
    def self.validate_options!(collection_id, options, in_action) #:nodoc:
      options.merge!(DEFAULT_OPTIONS) {|key, old, new| old}

      valid_options = DEFAULT_OPTIONS.keys
      valid_options << :actions unless in_action
    
      unknown_option_keys = options.keys - valid_options
      raise ActionController::ActionControllerError,
            "Unknown options: #{unknown_option_keys.join(', ')}" unless
              unknown_option_keys.empty?

      options[:singular_name] ||= Inflector.singularize(collection_id.to_s)
      options[:class_name]  ||= Inflector.camelize(options[:singular_name])
    end

(2)paginateからAR.countに:distinctが渡されない

#pagination.rb
    def count_collection_for_pagination(model, options)
      model.count(:conditions => options[:conditions],
                  :joins => options[:join] || options[:joins],
                  :include => options[:include],
                  :select => options[:count])
    end

という実装になっているためです。

そこで、下記のように強引にpaginate周りをいじる必要があります。

#pluginなどでクラスを再オープンして定義
module ActionController
  module Pagination
    DEFAULT_OPTIONS[:distinct] = false
    
    def count_collection_for_pagination(model, options)
      model.count(:conditions => options[:conditions],
                  :joins => options[:join] || options[:joins],
                  :include => options[:include],
                  :select => options[:count],
                  :distinct => options[:distinct])
    end
    
  end
end

クラス再オープンしている時点で終わってる気がします。もっとスマートなやり方知ってる人は教えてください。
なければRailsにパッチ送ろうかなぁ。

(Shouta)