2007年09月


目次


2007年09月25日

デファクトに乗っかりながらそれを超えるというRubyの話

ひがさんが思考停止を批判している

実際良く見かけるんだけど「最新の技術についていくのは疲れた」「なにかスーパーなデファクトが現れてそれで統一されて欲しい」「考えるのめんどくさいから標準で統一したほうが無難」なんてのは、おやじ予備軍シンドロームだと思う。

で、Railsにもその虞ありとのことだ。

Javaの世界はフレームワークが乱立していて適切な組み合わせを選ぶのが大変だからRailsで統一されているRubyがいいんだと。

これには、全然賛成できません。悩みたくないからスーパーなデファクトを望むのは、危険だと思うんですよね。ライバルがいないとどうしても進歩の速度が鈍ります。Rails一党独裁よりも、いくつかのフレームワークが競い合うほうが健全なのではないかと思います。

これについてのRailsサイドからの言い分はあかさたさんが「本筋には大賛成です。枝葉末節についてです。」と断った上で「Railsユーザーだっていろいろ考えているんだけどなぁ・・・」で述べていて、

前の Rails 勉強会では Merb というフレームワークが紹介されていました。こちらの記事が詳しいです。このフレームワーク自体はまだ実用には時間かかりそうですが、Rails ユーザも一党独裁問題に対しては、バランスを取り始めています。言い換えると、Rails ユーザはいい意味で Rails に対して批判的です。

うん。そう思う。で、以下はその「枝葉末節について」のさらに蛇足みたいな話だ。

Ruby的な何か

以前日経ソフトウェアに書かせていただいたけど、RubyやRailsにはPythonとはまた違った意味で拘束的な「これがこの文化のやり方だ」という何かがある。その記事では触れなかったけど、私はRSpecにもそれを感じていて、だからTest::UnitよりもRSpecが好きだ。

これらは「そのプロダクトが形作る世界にとって自然な考え方」というものがあって、その考え方の範囲でなら何をしても許される。その考え方からはみ出ることを誰も否定はしないけれども、それは恐ろしく不便な茨の道だ。そして、プロダクトは「自然な考え方」に沿う人を全力で支援してくれる。

そして、プロダクトの現在の実装は完成形ではない。「自然な考え方に照らしてどうよ?」と問えば開発陣は答えてくれる。

私は以前、RSpecの癖のある例の記法について会社でこう言った。

RSpecの記法は、なんとなくそれっぽく書けばいいんだよ。それっぽく書いてRubyとしてparseも通って、それでもうまく動かないならそれはRSpecが間違ってるからpatch送れ。

今ならpatchの前に「Matcher書け」というところだけれども。

Railsもそうだ。「これおかしくね?」と言って、それが開発陣の間に共有されている世界像にマッチすれば開発陣はもの凄い速度で動いていく。ActiveRecordなんかは特に「何となく書けば動く」し、それで動かなければプラグインかpatchを書くときだ。あ、これも会社で言ったことがあるなぁ。

ActiveRecordの呼び出しは基本を覚えたらあとはなんとなくそれっぽく書けばいいんだよ。それで動かなければRailsを直せ

と。Cacaded Eagar Loadingなんかはまさにそうして生まれたと思う。同様にして「Rails的にはこの分野ではこう動くべきだよね」という種類のプラグインもたくさんある。acts_asシリーズはまあそうだよね。

Rubyもそうだ。ruby-devではそんな議論がよく為されている。日経ソフトウエアの記事で言及しているように、「驚き最小の法則」は今は禁句だけど精神は生きている。生きているどころか、それがRubyの開発をリードし続けている。

Ruby way

Ruby/RSpec/Railsに、「デファクトを受け入れる」という姿勢は希薄だ。いや、無い訳じゃない。Lispを差し置いてRubyなんかが目立ってしまったのはデファクトスタンダードなALGOL風記法によるところも大きいだろう。ただ、今あるものを粛々と受け入れる姿勢はない。いまあるものの本質を理解して、それに反するものは本家本元であろうと容赦なく批判し、本家もそれを受け入れていく。それがRuby wayである。

ひがさんの論は「今Railsに飛びついている人にはそういう思考停止に陥る虞があるよね」だから、上記の私の話は微妙にずれた議論ではあると思う。けれども、Rubyに脈々と流れる気質はきっと思考停止の害毒を打ち消す方向に作用するだろう。


2007年09月22日

昔住んだ街を歩く

昔住んだ街に降りた。

見知った音、風、匂い、そして色の中を歩いていく。

けれども、ここにもはや私の帰る場所はない。一緒に住んだ人もいない。

今のここには私はいない。私は私の中の過去の街をさまよっている。

そして、あるものは変わり、失われている。

あま噛みされる痛みが身体に広がる。

記憶が降ってくる感覚に酔いながら通り過ぎる。


2007年09月20日

ミステリクロノ

ミステリクロノ』を読んだ。

久住四季が書いたと思わなければ面白かったんだけどなぁ。この人はもっと面白いものを書けると思ってるから読後に少々不満感が残った。トリックスターズの久住四季が書いた新シリーズである。前シリーズでは魔術+ミステリを描いて見せたが、今度は時間操作+ミステリということらしい。

設定を出していく部分が妙に説明的に見えたのは何故だろう。そう思って見返すと、実感ほどには説明的な書き方はしていないのね。やっぱりこの作者はそんな下手じゃない。どうして私はそう感じたんだろうと思いながらweb上の書評を漁っていくと、その原因が分かってきた。時間を操る「クロノグラフ」という設定があまりにもミステリを構築するのに従属しているのが見えすぎるんだ。

してみると、やっぱり前シリーズの「魔術」という選択肢はうまく機能していたんだなぁ。ライトノベルで使い古されたモチーフであるが故に、ライトノベルを偽装して道具立てを隠蔽できていた。今回は道具がよく見えすぎてしまって、読者たる私はついつい道具だてに目がいってしまったし、「騙された快感」がないんだなぁ。

で、今回は道具が見えすぎている割にトリックは小さめで。やっぱり、(***自主規制***)が(***自主規制***)なところで謎はそこまでは推理できちゃうよね。私は謎解きの中盤まで動機は推理できなかったけど。

書評を読んでもキャラクターに感情移入して読んだ人の評価はそう悪くないみたい。確かになー、トリックスターズよりはキャラクター小説的だろうか。そういう読み方をして積極的に騙される形のほうが、いっそトリックを楽しめるかも知れない。

ただ、設定解説ラッシュが終わった次作(続くと思われる)以降では(***自主規制***)したり(***自主規制***)したりっていうトリックの余地もあるし、多分「うまく騙してくれる」に違いない。期待して次作も買うつもりでいる。


2007年09月18日

Rails勉強会@東京 第22回

Rails勉強会に行ってきた。しばらく抑うつ症状やらパニック症状やらの状態が悪かったので、久しぶりの参加だ。でも、最初から最後までどうも頭が働いてなかった。処理が追いつかなくて、切り返しが鈍かったり反応を返せなかったりした人にはごめんなさい。医師には「もう暫くゆっくりしなさい」 と言われてしまったけど、なるほどまだ本調子じゃないね。

うむ。気がつけばこの勉強会も回数を重ねて、もうすぐ2周年。いつもの通りの形式である。

前半セッション

3つのセッションに分かれた。

  • 初心者セッション(acts_as_authenticated)
  • Rails悩み事相談室
  • Ruby Hoedown 2007の動画を見る

私は初心者セッションに出た。オーナーは諸橋さん。とはいえ、私も我が物顔で口を挟ませてもらった。


acts_as_authenticatedとは

acts_as_authenticatedとは、Railsアプリケーションにログイン認証機能を提供するプラグインである。ユーザー登録、ログイン認証、ログアウトなどの機能は広く様々なアプリケーションで使われるであろうが、毎度毎度これを書くのは結構面倒である。acts_as_authenticatedはこの部分を簡単にしかも枯れていて安全な形で実装してくれる。

実際、ログイン機能というのは定型パターンであるのに手で書こうとすると結構面倒なものだ。ViewからDBまでの各レイヤーにわたってセキュリティに関する配慮は勿論求められる。かつ、ロジックの記述はDRYでありたい。変更の整合性がとれなくてこれほど困る部分もないからだ。と考えて真面目にやっていくと意外と手間が掛かる。acts_as_authenticatedはここの部分を簡単に解決してくれる。

以前はこの目的のプラグインというとLogin Engineが主流であったが、Rails 1.2以降ではEngineシステムが動かなくなった。Login Engineもメンテナンスは停止している。そういうことで、今ではacts_as_authenticatedが認証用プラグインの筆頭である。

rails-meeting22-1.20070916.png

rails-meeting22.20070916.png


インストールと構成

インストール方法は至って普通のRailsプラグインである。いつものごとく、

$ ruby script/plugin install http://svn.techno-weenie.net/projects/plugins/acts_as_authenticated

これで、vendor/plugins/acts_as_authenticatedにプラグインが格納される。このプラグインはscript/generateに対して新しい2つのジェネレータを提供する。

  • authenticated: アカウント登録/ログイン機能を生成
  • authenticated_mailer: アカウント登録後のメールによるアクティベーションの機能を生成

今回は、authenticated_mailer部分については割愛した。

acts_as_authenticatedの特徴の1つは、Railsのランタイムに外から作用するのではなく、generatorを提供するに留まるということである。生成されるのは幾らか癖はあるにせよ普通のRailsのMVCであるから、ユーザーはRailsアプリケーション開発に関する知識を活用してそれを自由にカスタマイズできる。


使い方

プラグインが提供しているauthenticatedジェネレータを利用する。

$ ruby script/generate authenticated MODEL CONTROLLER

ここで、MODELというのはモデルクラスの名前、CONTROLLERというのはコントローラの名前である。例えば次のようになる。

$ ruby script/generate authenticated User Account
      exists  app/models/
      exists  app/controllers/
      exists  app/helpers/
      create  app/views/account
      exists  test/functional/
      exists  test/unit/
      create  app/models/user.rb
      create  app/controllers/account_controller.rb
      create  lib/authenticated_system.rb
      create  lib/authenticated_test_helper.rb
      create  test/functional/account_controller_test.rb
      create  app/helpers/account_helper.rb
      create  test/unit/user_test.rb
      create  test/fixtures/users.yml
      create  app/views/account/index.rhtml
      create  app/views/account/login.rhtml
      create  app/views/account/signup.rhtml
      create  db/migrate
      create  db/migrate/001_create_users.rb

ユーザー情報を表すモデルクラスUserUserを操作するためのAccountController、及び付随するview、テストケースが生成された。

モデルクラスに対応するマイグレーション(db/migrate/001_create_users)が生成されているのでこれを見てみよう。

class CreateUsers < ActiveRecord::Migration
  def self.up
    create_table "users", :force => true do |t|
      t.column :login,                     :string
      t.column :email,                     :string
      t.column :crypted_password,          :string, :limit => 40
      t.column :salt,                      :string, :limit => 40
      t.column :created_at,                :datetime
      t.column :updated_at,                :datetime
      t.column :remember_token,            :string
      t.column :remember_token_expires_at, :datetime
    end
  end
  def self.down
    drop_table "users"
  end
end

生成した時点ではまだマイグレーションは実行されていない。今のうちに、好きなようにこのマイグレーションを書き換えることもできる。例えば、ユーザー情報として上記の他にニックネームが必要であるならそのカラム定義を加えればよい。余分なカラムがある分にはacts_as_authenticatedの動作に差し障ることはない。

私が以前acts_as_authenticatedを使った際、ログイン名はメールアドレスで兼用していた(この方針についてセキュリティ上の是非はあるだろうが、今は勘弁して欲しい)。だから、loginカラムの定義は削ってしまった。それでも後でちょっと手を加えるだけで問題なく動作させることができた。

既存のユーザー管理テーブルがあるなら、やはりマイグレーションやモデルクラスをいじくって何とかしてやればそのテーブルをacts_as_authenticatedと共存できる。

さて、ここではそのまま変更せずにこのmigrationを実行しよう。

 $ rake db:migrate
 == CreateUsers: migrating =====================================================
 -- create_table("users", {:force=>true})
     -> 0.0833s
 == CreateUsers: migrated (0.0835s) ============================================

これで、usersテープルが作成された。これでもう認証機能を利用できる。その様子を撮ったのが冒頭のスクリーンキャプチャである。素っ気はないが、機能は十分である。

解剖

acts_as_authenticatedが生成したAccountControllerの中身を見てみよう。

まず目立つのは、冒頭のこの1行。

  include AuthenticatedSystem

acts_as_authenticatedはAuthenticatedSystemモジュールを通じてコントローラーに認証機能を提供する。AuthenticatedSystemモジュールはlib/authenticated_system.rbに生成されている。認証の詳しい方法を変更したりするには、このモジュールをいじくれば良い。

アカウント登録

さて、ではユーザーアクションに沿って順に見ていこう。アカウント登録するには、http://localhost:3000/account/signupにアクセスする。

  def signup
    @user = User.new(params[:user])
    return unless request.post?
    @user.save!
    self.current_user = @user
    redirect_back_or_default(:controller => '/account', :action => 'index')
    flash[:notice] = "Thanks for signing up!"
  rescue ActiveRecord::RecordInvalid
    render :action => 'signup'
  end

最初の2行はRailsでモデルオブジェクトを編集するときの頻出パターンだね。と、まあ初心者セッションだから、その辺もフォロー。今、何もパラメータは渡していないのでparams[:user]nilだけど、とにかくそれで無理矢理Userオブジェクトを作ってしまう。で、GETでアクセスしているから、この場合は2行目でreturnする。

return後にレンダリングされるテンプレート(app/views/account/signup.rhtml)を見てみるとこうなっている。

<%= error_messages_for :user %>
<% form_for :user do |f| -%>
<p><label for="login">Login</label><br/>
<%= f.text_field :login %></p>
<p><label for="email">Email</label><br/>
<%= f.text_field :email %></p>
<p><label for="password">Password</label><br/>
<%= f.password_field :password %></p>
<p><label for="password_confirmation">Confirm Password</label><br/>
<%= f.password_field :password_confirmation %></p>
<p><%= submit_tag 'Sign up' %></p>
<% end -%>

さっき、空のUserオブジェクトを作って@userに設定しておいたお陰で、form_forヘルパーを使って楽に記述できる。

さて、このテンプレートで生成されるフォームは元と同じaccount/signupに対してPOSTするように書かれている。ポストするとどうなるか。再び、signupアクションの定義に戻る。

  def signup
    @user = User.new(params[:user])
    return unless request.post?
    @user.save!
    self.current_user = @user
    redirect_back_or_default(:controller => '/account', :action => 'index')
    flash[:notice] = "Thanks for signing up!"
  rescue ActiveRecord::RecordInvalid
    render :action => 'signup'
  end

今度は、フォームの値が渡ってきてparamsに入っている。このとき、次のような値を送信したとしよう。

rails-meeting22-3.20070916.png


Railsの魔法によりフォーム入力は解析されて次のようなHashによる構造に自動的に変換される。

{
  "user" => {
    "password_confirmation"=>"test",
    "login"=>"hoge",
    "password"=>"test",
    "email"=>"test@localhost"
  },
  "commit"=>"Sign up",
  "action"=>"signup",
  "controller"=>"account"
}

で、params[:user]を使って、今度こそUserオブジェクトを作るのだ。2行目においても、今度はPOSTメソッドでアクセスしているからここではreturnしない。

で、@user.save!する。ジェネレータが生成した段階でUserクラスには細かくvalidationが設定されている。そのため、正しい登録情報であれば保存され、何かが間違っていれば例外が発生する。今は正常系を見ていくとしよう。

current_userという属性はAuthenticatedSystemモジュールの属性で、後で見るようにログイン管理の中核を為す。acts_as_authenticatedの基本的な仕組みとしては、この属性にユーザー情報オブジェクトが入っていれば"ログイン状態"、そうでなければ"未ログイン状態"ということになっている。つまり、ここではユーザー登録と同時にログイン状態に移行してしまうわけだ。

そして、「元のページ」または/accountにリダイレクトする。以上。リダイレクト先はアプリケーションに合わせて自由に修正すればよい。

ここで質問が出た。

入力が不正な場合はどうなるのか

validationに失敗して、例外が発生する。最後のrescue節がこれを捕まえて、再びapp/views/account/signup.rhtmlをレンダリングする。error_messages_forヘルパーやなんかの働きでこんな感じに表示される。

rails-meeting22-4.20070916.png


ユーザー情報の保存

さて、上ではユーザー情報の保存されるところをsave!で保存されるとだけ流してしまったが、その中身を詳しく見てみよう。app/models/user.rbを見る。

冒頭にはvalidationが沢山。これが登録時の入力エラーなんかを弾いてくれる。

  validates_presence_of     :login, :email
  validates_presence_of     :password,                   :if => :password_required?
  validates_presence_of     :password_confirmation,      :if => :password_required?
  validates_length_of       :password, :within => 4..40, :if => :password_required?
  validates_confirmation_of :password,                   :if => :password_required?
  validates_length_of       :login,    :within => 3..40
  validates_length_of       :email,    :within => 3..100
  validates_uniqueness_of   :login, :email, :case_sensitive => false

ここで、「ん? password」と思った人は偉い。さっきmigrationを見たとき、passwordなんていうカラムはなかった筈だ。だから、そのままならUserオブジェクトにもpasswordなんていう属性は定義されない。

crypted_passwordカラムならあった。実はacts_as_authenticatedではパスワードのSHA1ハッシュだけを保存するのだ。生パスワードは保存しない。ま、これはセキュリティ上の定石ってやつだけど、このあたりの解説をちゃんとセッション予定に組み込んでた諸橋さんは偉い。Rails初心者と言ってもJava経験者だったりすると知ってるかも知れないけど、経歴は様々だから、確かにセッションオーナーは配慮したほうがいいよね。

さて、じゃあcrypted_passwordに対して生パスワードと思われるpassword属性はどこにあるんだろう。見回すと、Userクラスの冒頭で明示的に定義している。

  # Virtual attribute for the unencrypted password
  attr_accessor :password

そうなのだ。実はRailsのvalidationはActiveRecordで自動生成された属性以外にも使える。その属性がありさえすればよい。

さてと、validationのすぐ下にこんな記述がある。

before_save :encrypt_password

これが登録時の鍵だ。この記述によりUserオブジェクトを保存する前には必ずencrypt_passwordメソッドが呼ばれる。その中身を見てみよう。

    def encrypt_password
      return if password.blank?
      self.salt = Digest::SHA1.hexdigest("--#{Time.now.to_s}--#{login}--") if new_record?
      self.crypted_password = encrypt(password)
    end

これが生パスワードであるpassword属性からcrypted_password属性の値を作り出す。仕組みはこう。

  1. passwordが空なら何もしない。
  2. ユーザー毎に一意っぽいsaltを作る。saltっていうのは、事前計算攻撃に対する対策。ハッシュ値と共にDBに保存される。
  3. で、encryptメソッドでcrypted_passwordを作る。

encryptは、というと

  # Encrypts the password with the user salt
  def encrypt(password)
    self.class.encrypt(password, salt)
  end

作っておいたsaltを使ってクラスメソッドのUser::encryptを呼ぶ。更にその中身。

  # Encrypts some data with the salt.
  def self.encrypt(password, salt)
    Digest::SHA1.hexdigest("--#{salt}--#{password}--")
  end

受け取ったsaltpasswordを使ってSHA1ハッシュを作ってるだけ。

分散してるので面倒だけど、これは最初に言ったロジックをDRYに保つため。要は、オブジェクトの保存前に生パスワードからsalt付きのハッシュを計算するというだけだ。以上が、acts_as_authenticatedのアカウント登録の流れであった。

ログイン

では、登録後にログインするところを見てみよう。ログインには、/account/loginにアクセスすればよい。loginアクションの中身を見てみよう。

  def login
    return unless request.post?
    self.current_user = User.authenticate(params[:login], params[:password])
    if logged_in?
      if params[:remember_me] == "1"
        self.current_user.remember_me
        cookies[:auth_token] = { :value => self.current_user.remember_token , :expires => self.current_user.remember_token_expires_at }
      end
      redirect_back_or_default(:controller => '/account', :action => 'index')
      flash[:notice] = "Logged in successfully"
    end
  end

remember_meがどうとか書いてあるのはセッション終了後もログイン状態を保つ機能、よく「ログインを保つ」とか書いてある機能を提供するためのものだ。今回は深くは立ち入らなかった。その部分を割愛すればこのメソッドはシンプルだ。

  def login
    return unless request.post?
    self.current_user = User.authenticate(params[:login], params[:password])
    if logged_in?
      redirect_back_or_default(:controller => '/account', :action => 'index')
      flash[:notice] = "Logged in successfully"
    end
  end

POSTメソッドでなければそのまま戻るのはsignupのときと同じ。これでログインフォームがrenderされる。

で、そのフォームから再び、今度はPOSTで戻ってきたとき。User::authenticateというクラスメソッドが認証処理の実体らしい。

多少書き換えると、こうなっている。

  # Authenticates a user by their login name and unencrypted password.  Returns the user or nil.
  def self.authenticate(login, password)
    u = find_by_login(login) # need to get the salt
    u && (u.crypted_password == u.encrypt(password) ? u : nil
  end

やってることは簡単だ。

  1. ログイン名に基づいてUserオブジェクトを取得。ActiveRecordのdynamic finderを使ってる。
  2. - 該当するレコードがあればUserオブジェクトが、そうでなければnilが返る。
  3. 次の条件を満たすとき、uを返す。そうでなければnilを返す。
  4. - uUserオブジェクトであってnilではないこと
  5. - uの保存されているハッシュと、今入力された生パスワードを同じ手順でハッシュ化したものとが、一致する

ふーん。それじゃあ、またAccountControllerloginアクションに戻ろう。

  def login
    return unless request.post?
    self.current_user = User.authenticate(params[:login], params[:password])
    if logged_in?
      redirect_back_or_default(:controller => '/account', :action => 'index')
      flash[:notice] = "Logged in successfully"
    end
  end

authenticateメソッドは、ログイン名とパスワードが一致すればそのユーザーを表すUserオブジェクトを、さもなくばnilを返すのであった。それで、それをcurrent_user属性に設定。

前に書いたように、内部的には「current_userUserオブジェクトが設定されている」== 「ログイン状態である」ということなのであった。これでログイン終わり。

諸橋さんは、このアクションでセッションを切り替えないとセッション固定攻撃されるんでないかと言ってた。私も同感。ここはパッチを送る必要がありそうだ。

ログイン要求/判別

AccountControllerはこれで良いとして、他のControllerでログインを要求するにはどうしたら良いか。

class HogeController
  include AutheticatedSystem
  before_filter :login_required
  ....
end

これだけである。これで、HogeController配下のアクションでは全てログインが必要となる。未ログイン状態でアクセスすると/account/loginにリダイレクトされ、そこでログインすると元のアクションに戻ってくる。

初心者セッションなのでフィルタを知らない人もいた。Railsにおけるフィルタっていうのは、まぁ、JavaのServlet APIのフィルタと同じようなもんで、リクエストをアクションメソッドが受け付ける前、あるいは後に処理を挟み込むことができる。入出力を加工したり、特定の条件下では本来のアクションメソッドへ処理を移さずに他へとばしたりもできる。

対象となるアクションを絞り込むこともできる。この例では、特定のアクションにだけログインを要求したり、特定のアクションだけログインしなくても良いようにしたり。それにはこんな構文をつかう。

  before_filter :login_required, :only => [:foo, :bar]

とか

  before_filter :login_required, :except => [:foo, :bar]

とか。

ふーん。で、じゃあ、login_requiredフィルタの中身を見てみよう。ここでログイン状態判別の方法を見ることができるはずだ。実装はlib/authenticated_system.rbにある。

    def login_required
      username, passwd = get_auth_data
      self.current_user ||= User.authenticate(username, passwd) || :false if username && passwd
      logged_in? && authorized? ? true : access_denied
    end

User::authenticateはさっき見たよね。細かいことはさておき、とにかくやっぱりself.current_userが肝なのだ。じゃあ、current_user属性の実装を見てみよう。

    # Accesses the current user from the session.
    def current_user
      @current_user ||= (session[:user] && User.find_by_id(session[:user])) || :false
    end
    
    # Store the given user in the session.
    def current_user=(new_user)
      session[:user] = (new_user.nil? || new_user.is_a?(Symbol)) ? nil : new_user.id
      @current_user = new_user
    end

getterを見てみると、ちょっとコードが技巧的だ。

  • インスタンス変数@current_userが設定済みならそれが活きる
  • そうでなければsession[:user]を見て、セッション変数が設定されているようならそれをUserオブジェクトのidと見て取得を試みる。
    • 取得成功すれば、それを@current_userに保存しておく。
    • そうでなければ、:false@current_userに設定しておく。
  • いずれにせよ、ここまでで設定されている@current_userの値を返す。

ここで、:falseなんていう変なシンボルを使ってる意味がわかんないよねー、と私と諸橋さんはセッションでぶちぶち言ってたけど、今気がついた。あちこちで@current_user ||=というコードが実行されることになるから、「ログイン状態未知」と「非ログイン状態」を区別する必要がある。ここで@current_usernilとかfalseを入れておくと本来は「非ログイン状態」であることは既知の筈なのに、毎回ログインを試みて失敗することになり無駄なクエリが走るのだ。それで、「無効値であることが見た目に分かりやすく、かつ、Rubyにとっては真として評価される」値が必要なんだ。

setterのほうは、これは前に書いたようにUserオブジェクトまたはnilを設定されるものと想定している。is_a? Symbolしてるところを見ると:falseも受け付けるみたいね。まず最初は、

  • 有効値(Userオブジェクト)を与えられたなら、そのidをセッション変数に設定
  • 無効値を与えられたなら、セッション変数にnilを設定

で、@current_userにも値を保存しておく。と、

これでget/setできる@current_userをどこで使ってるかというと、主にAuthenticatedSystem#logged_in?で使ってる。

    # Returns true or false if the user is logged in.
    # Preloads @current_user with the user model if they're logged in.
    def logged_in?
      current_user != :false
    end

おー、シンプル。うん、さっき見たcurrent_userの定義で、必ずUserオブジェクトから:falseが返るようになってるから、:falseと比較するだけでログインしているか判別できるのだ。

で、このlogged_in?をあちこちで使ってる訳ね。これが、ログイン判別の仕組みであった。



後半

  • 初心者向け(Scaffoldの先)
  • Componentの改善案を探る
  • 某Ruby書籍翻訳査読会

最初のやつはyuum3さんがやってくださった。ハンズ・オン形式で、scaffoldは分かったけど、その先何も分からないという人のためのセッション。

次のは、render_componentが遅くてかなわないので改善案を考えようというセッション。

私は、自分で持ち込んだ某書籍査読会のオーナーになった。内容はないしょ。とりあえず、みんなで楽しんだとだけ言っておこう。


懇親会

jig.jpの人とか来てた。携帯上の開発は別世界なので聞いてて面白い。クラスローダの無いJava!

与太話

懇親会で、諸橋さん瀧内さんとも話した。

大体このあたりで話すといつもRailsアプリケーションのパフォーマンスをどうしてるかとかそういう話になる。瀧内さんが「cascaded eagar loadingを5段7段と重ねるとメモリーを喰ってそれが足を引っ張る」「でもABD的にやるとどうしてもそんな感じのクエリが必要」という話をしてて、諸橋さんがキャッシュの話をしてて。

で、私がまた電波を飛ばし始めた。「DBサーバーとアプリケーションサーバーの間にもう1つサーバーを置けばいいよね」と。まぁ、例によって思いつきベースの与太話なんだけど。

アプリケーションロジックをDBサーバーに任せる発想というのは私は好きで、だから私はストアドプロシージャ大好き。だけれど、それでDBサーバーに負荷が増えるのは嬉しくない。だいたいが、Railsアプリケーションの並列化ではアプリケーションは殆どshared nothingなんだけど、その分がDBに集中するんだよね。なら、APPとDBの間にもう一層おいて、そこでできるだけキャッシュして、そこで必要なトリガを引いて、そこにビジネスロジックを盛り込んで、APPサーバーはそのサーバーを参照すればいいじゃない。dRubyで。

特に、Railsが得意とするような「今からCREATE TABLEします」っていう開発ではDBを参照している既存のアプリケーションというのは無くてこれから全てを作るのだから、このやり方で無理がない。

と、発想は出てくるけど、考えれば考えるほど「それ、なんて言うEJB?」なんだ。

でもね、私は今実際にそういう形で動かしてる。キャッシュや、データ変更に対するObserverを保持するサーバープロセスがmongrelの裏に3つぐらい動いてる。今のところはキャッシュが無効化される頻度が極めて少ないので手で裏方サーバーを再起動してキャッシュクリアしてるけど、これを発展させて、より洗練していけばそこにたどり着く。

結局、EJBは悪くないんだ。ロジックが単独のWebアプリケーションに収まらないとき、あるいは遅延させてバックグラウンドで動かすとき。そういうときにロジックを裏方サーバーで動かしておくのは悪くない。

全部が全部リモート扱いじゃパフォーマンスに響くからEJBはLocalインターフェースを作った。でも、結局インターフェースを定義する手間は変わってない。EJB3になってPOJOになって、Homeインターフェースも作らなくて済むようになって、でもやっぱり分散を視野に入れた定義は必要で。

それは、初期段階ではover killなんだ。自社でB2Cサービスを提供して勝ち残っていこうとするような我々、インターネット業界の住人にとってはover killだ。我々に必要なのはいち早くアプリケーションが動きはじめて、エンドユーザーに見てもらえること。そのためにはRailsは良い道具だ。分散がどうこうとか、そういうのは必要がないし、そのためのわずかなコストも惜しい。

そして、サービスが成長していったとき、そのときになって自然な形でロジックを裏方に委譲できる仕組みが欲しい。dRubyにはその潜在能力がある。私はRubyは好きで、個人的にはずっと使っていくだろう。でも、仕事でRubyを使っているのはRailsとdRubyがあるからだ。dRubyの力で、モデル層の後ろ半分をリモートプロセスに自然に引きはがせるような、そういう仕組みが欲しい。作れるはずだ、いつか作ってやる。

とか、そんなことを話した。


2007年09月17日

Rubyistの誕生

ruby-listの過去ログ漁ってたら"Rubyist"という言葉が誕生した瞬間を発見してしまった。意外と新しい、のか?

[ruby-list:397] Re: about exception

rubyist(ルビイスト)というのはどうでしょう?

まつもと ゆきひろ /:|)


2007年09月15日

シンボルとは何か その1(前編) - 文字列の同一性と同値性

シリーズ・RubyのSymbol

とりあえず分かりやすいものから見る、ということで、「intern化された文字列」としてのシンボルを説明する。そのためにはRubyにおける文字列値について詳しく考える必要がある。

同一性と同値性

次は真だろうか。

"abc" == "abc"

勿論、真である。RubyではString#==はオーバーライドされていて、長さと内容†1が等しい文字列同士を比較すると真になる。

さて、次は真だろうか。

"abc".equal? "abc"

これはたぶん偽だろう†2。それは何故か。Object#equal?は引数のオブジェクトがレシーバ自身である、まったく同一のオブジェクトを指すときのみ、真となる。

equal?のようにオブジェクトとして等しいことを「同一性」と言う。これに対してString#==のように示す内容が等しいことを「同値性」と言う。

Javaプログラマはこう考えると分かりやすいだろう。同一性とはJavaにおけるこれである。

str == "abc"

同値性とはJavaにおけるこれである。

str.equals("abc")

表記がRubyとほぼ逆なのがややこしいけど、まあ、文字列比較を==でやると死ねるというのはJavaプログラマは慣れているだろう。

同一ならば必ず同値でもある。自分自身とは同値、ということだ。当たり前である†3

けれども、同値でも同一とは限らない。同じ文字列を表すStringオブジェクトでも、別のオブジェクトかも知れない。だからJavaでは面倒でも文字列比較にはString#equalsを使うのだ。

ま、分かってる人にはつまんない話だったね。


文字列リテラルの指すもの

さて、では次は真を返すか。

buf = []
2.times do
  buf << "abc"
end
puts buf[0].equal?(buf[1])

真と答えた人は残念。たぶん偽である†4。Javaだったら、たぶん真なのだが。

String[] buf = new String[2];
for (int i = 0; i < 2; ++i) {
    buf[i] = "abc";
}
System.out.println(buf[0] == buf[1]);

同一箇所の同一のリテラルを代入したのに、Rubyでは何故違うオブジェクトなのか。Rubyは"abc"というリテラルを評価する度に、毎回新しい文字列オブジェクトを生成して返すからだ。どうしてそんな無駄なことをするのか。

実際Javaだと、この文字列リテラルはコンスタントプールの中にある唯一の文字列を差し続ける。毎回、同じindexでldcやなんかをするのね。

Rubyがこうやっている理由は、文字列の式展開が可能で、しかも文字列はmutableだからだ。だって、プログラムがこうだったらどうする?

buf = []
2.times do
  buf << "abc"
end
buf[0][0] = ?z
puts buf[0], buf[1]

JavaではStringはimmutableだ。一度生成された文字列オブジェクトの値は変えられない。他の値が欲しければ別の文字列オブジェクトを作るしかない。だから、コンパイラは最適化のため、同一内容の文字列ならできるだけ同一のオブジェクトを使い回すようにできる。その辺にある普通のjavacは当然そうする。

しかしRubyでは文字列オブジェクトの値は変更できる。だから、今は同一内容でもあとでそうでなくなるかも知れない。故に、リテラルといえども、評価する度に新しいオブジェクトを生成しないとおかしくなる。

まして、式展開なんか含んでいたら。

buf = []
2.times do
  buf << "abc #{Time.now}"
  sleep 1
end

ま、"abc" == "abc"ぐらいだったら文字列を書き換えていないことは明らかなので、処理系が気を利かせてJavaみたいな最適化をしても良いのではあるけど。でも、プログラム全体にわたって本当に書き換わることがないと断言するのは、実用上は困難だ。それはすごく難しいコンピュータサイエンスの問題だと思うから、ささださんとか処理系研究者の人に期待。

余談

実はCでもRubyと似たような問題は発生する。Cでは文字列はmutableだからだ。

#include <stdio.h>
int main(void)
{
    char *buf[2];
    int i;
    for (i = 0; i < 2; ++i)
    {
        buf[i] = "abc";
    }
    buf[0][0] = 'z';
    printf("%s, %s", buf[0], buf[1]);
    return 0;
}

でもCはRubyのように親切ではない。こういうことは、やる奴が悪いということになっている。結果は「未定義」 である。

古き良き時代の環境だったら、たぶん"zbc, zbc"と出たんだろうけどね。今どきのメモリ保護の効いた環境で動かすと異常終了するケースが多いんじゃなかろうか。ま、これがコンパイル通るあたりは「自分の足を撃ち抜く自由」というやつか。

予告

シンボルとは何か その1(後編) - 文字列のintern」に続く

  • †1: と、Ruby 1.9以降ではエンコーディングも
  • †2: 私はこれは「実装依存」だと思うけど、何しろRubyにはそのところの言語仕様が明文化されていないのでよく分からない。2007年9月15日現在、最新版のCRubyとJRubyでは偽だった
  • †3: その辺につっこんで行くのが数学屋の習性だけど
  • †4: 私はこれも「実装依存」であるべきだと思うけど

シンボルとは何か 概論

Rubyにはシンボルというものがある。:thorughとか:conditionsとか、コロンで始まる表記で、RailsのAPIによく出てくるアレだ。

でも、シンボルって何だろう。この問いに答えられる人は少ないのではないか。シンボルリテラルの表記法は分かる。Symbolクラスのインスタンスであることも分かる。何だか、文字列に似ているのも知っている。で、それは何?

この記事シリーズは、シンボルとは何かという問いに私なりの回答を提示する。

視点

Rubyにおけるシンボルを、私は3つの視点から説明する。

  1. 意味論から言えば、「プログラムにおける名前」という概念に対応するオブジェクトである。
  2. 他のオブジェクトとの相互作用で言えば、「intern化された文字列」に近い
  3. 実装から言えば、Rubyの名前管理用ハッシュ表のエントリである。


シリーズ目次

  1. その1(前編) 文字列の同一性と同値性
  2. その1(後編) 文字列のintern
  3. 以下、続く。

クリエイティブ・コモンズで貢献的スパムな提案

しかし何だな。訳わかんない自動生成されたリンク集みたいなスパムサイトのお陰で、だいぶgoogleサーチも不便になった。

その手の連中はリンクファームとかドメイン名のページランク上昇とか、そういうのが目的なんだろう。どうせならクリエイティブ・コモンズ商用利用可な技術文書を引っ張って公開すればいいのに。コンテンツ自体は良質だから検索した人には役に立つし、連中としてもグーグルでの順位が割と簡単に上がるだろうし、コミュニティとしてもバックアップの役にも立って一石三鳥だと思うんだけど。


2007年09月14日

i-revoのApacheがタコな件

i-revoさん、RubyKaigi2007では動画、無線LANともに大変お世話になりました。こういう一見地味なことを着実に安定に提供する技術力は素晴らしいと思います。でも、御社のadminはタコです。

% curl -I http://labs.i-revo.jp/pub/rubykaigi2007/rubykaigi2007-d1-t1-s00.wmv
HTTP/1.1 200 OK
Date: Fri, 14 Sep 2007 03:22:01 GMT
Server: Apache/2.2.4 (Unix) PHP/5.2.1
Set-Cookie: irtrack=122.249.96.213.1189740121439932; path=/; expires=Tue, 13-Nov-07 03:22:01 GMT; domain=.i-revo.jp
Last-Modified: Mon, 18 Jun 2007 13:27:31 GMT
ETag: "2c009f-1e78afd-28b056c0"
Accept-Ranges: bytes
Content-Length: 31951613
Content-Type: text/plain; charset=UTF-8


% curl -I http://labs.i-revo.jp/pub/rubykaigi2007/rubykaigi2007-d1-t1-s00.mp4
HTTP/1.1 200 OK
Date: Fri, 14 Sep 2007 03:24:11 GMT
Server: Apache/2.2.4 (Unix) PHP/5.2.1
Set-Cookie: irtrack=122.249.96.213.1189740251582143; path=/; expires=Tue, 13-Nov-07 03:24:11 GMT; domain=.i-revo.jp
Last-Modified: Mon, 18 Jun 2007 13:27:24 GMT
ETag: "2c00a9-147c19b-28458700"
Accept-Ranges: bytes
Content-Length: 21479835
Content-Type: text/plain; charset=UTF-8

世の中、IEみたいなふざけたクライアントばっかりじゃないんだYO! 特にRubyistみたいな変態の集まりでは。今回の動画公開フォーマットの選定にあたってはたださん(だったかな?)akrさんがオープンなフォーマットであることに強くこだわったと伝え聞いておりますが、そういうことから何も学ばれなかったようで慶賀の至りです。(© mput)

ブラクラは本当に勘弁。

そういう訳でついカッとなって書いた記事だけれども、でも駄目なものは駄目、って誰かが言わないと。


2007年09月11日

Exception Notifier Pluginを導入して分かったこと

運用環境でRailsのException Notifierを使ってる。

で、アプリケーションでUnhandledな例外が発生するとメールが飛んでくる訳だ。分かったのは、運用段階まで残るエラーは結局nilに対するNoMethodError、つまりは所謂「ぬるぽ」が多いということ。だから、これからはモデルがnilを返した場合、Viewにnilが渡った場合の挙動についてもっと重点的にSpec記述すべきということだ。

もう1つ言えるのは、今の開発体制においては言語の柔らかさは障害になってないということだ。動的型付けのメリットが活きていて、デメリットはちゃんとRSpecを記述することでカバーできている。「ぬるぽ」はどうせJavaやC#程度では、「言語の固さでカバー」という訳にはいかないものね。型システムの固さでカバーしようとするなら必要なのはJava/C#程度じゃなくてMaybeモナドとかそういう無効値を許さないシステムだ。

そういうわけだから、VMが枯れてるとかパフォーマンスとかではJavaやC#を羨む余地もあるけれども、言語の柔らかさそのものは「やっぱりRubyでいいんだ」と再確認したのだった。


2007年09月10日

MacOS Xのtelldir/seekdir

MacOS Xのtelldir(3), seekdir(3)の挙動がちょっと面白かった。

今、ls -lするとこうなるディレクトリにいるとしよう。

total 40
drwxr-xr-x   2 yugui  yugui     68  9  7 22:16 1.d/
drwxr-xr-x   2 yugui  yugui     68  9  7 22:16 2.d/
drwxr-xr-x   2 yugui  yugui     68  9  7 22:16 3.d/
drwxr-xr-x   2 yugui  yugui     68  9  7 22:16 4.d/
-rwxr-xr-x   1 yugui  yugui  13484  9 10 21:51 test*
-rw-r--r--   1 yugui  yugui    537  9 10 21:51 test.c

ここで、こんなプログラムを動かしてみる。

#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>

int main(void)
{
    DIR *dp;
    struct dirent *pent;

    dp = opendir(".");
    printf("%ld\n", telldir(dp));
    printf("%ld\n", telldir(dp));
    printf("%ld\n", telldir(dp));
    printf("%ld\n", telldir(dp));
    seekdir(dp, telldir(dp));
    puts("---");
    while ( (pent = readdir(dp)) != NULL )
      {
        printf("%ld: %s\n", telldir(dp), pent->d_name);
        telldir(dp);
        telldir(dp);
        telldir(dp);
        telldir(dp);
        seekdir(dp, telldir(dp)-5);
      }
    return 0;
}

要は無用にtelldirしてるだけ。結果はこうなる。

1
2
3
4
---
6: .
12: ..
18: 1.d
24: 2.d
30: 3.d
36: 4.d
42: test
48: test.c

  • telldirすると毎度ディレクトリストリームのカウンタが進む。
  • 別に最後に呼んだときのカウンタ値以外をseekdirに渡してもOKみたい。

WindowsでMSVCRTとか、Linuxとかだとtelldirは何度呼んでも次にreaddirするまで同じ値を返し続けるんだけど。その感覚からするとtelldirでカウンタが進むのはちょっと驚き。BSD系ってこういうもの? それとも、HFS+の仕様?

manに書いてある通りに使えば問題はない。けれども、manを無視して「どうせオフセット値」とか思って悪いことをすると嵌りそうだ。大昔の、struct direntが実際には配列の要素だったころとは随分違うんだね。


悪夢?

夢を見た。

何者かに追われていて、とうとう追い詰められた。そこで私はおもむろに鞄の中から溜まったコンビニのレシートを取り出して、「ならばこれをやろう」と手渡す。

相手は何か重要なヒントでも書かれているのかと読むけれども、書いてあるのは単にペットボトル147円とかそんなのだ。ふざけるな、と相手はレシートを投げ捨てる。私は「道に捨てちゃ駄目でしょっ」と問い詰めて、ひるむ相手を追いかけていく。そんな夢を見た。

こうもふざけた夢を見るのは「暴れん坊将軍」以来だな。


2007年09月08日

ヱヴァンゲリヲン見た

今日は通院日。病院がすさまじく混んでいて、すっかり遅くなった。コンディションも珍しく良かったので、遅くなりついでに「ヱヴァンゲリヲン 序」見てきた。

見た人がラミエルがカワユスと言ってる意味が分かった。あれは不変量あるのかとか計算したくなるよね。『海を見る人』みたいに計算したくなるSFやはりあって。こういうのはいいSFだよね。先週から殿方がラミエル、ラミエルと騒いでたのはフロイド的理由で砲撃戦が好きなだけじゃなかったんだ。

でも、やっぱりサキエルが一番かわいいと思う。なるほど、前劇場版を思わせる暗示はあったけど、何にしてもこれ単体では評価しちゃいけないな映画だよね。特に、前エヴァを見てない人は評価に困るだろう。だから、サキエルかわいい。シャムシェルきもちわるい。ラミエル計算。カヲルくんwktk、ということで、ひとまず。


2007年09月06日

どんなに人気があろうと言葉を選べる人であろうと駄目なものは駄目、って誰かが言わないと。

どんなに技術があろうと出来る人であろうと駄目なものは駄目、って誰かが言わないと。」にインスパイアされました。


例えばamebloのフィードが言葉を選んでようがどうだろうがCSS Niteがおしゃれな存在だろうがどうだろうが仁義に反するものは駄目。駄目なものは駄目。違うか!?

少なくともあなたの発信情報はH○○P 1.1だったり懇○会の席の発言だったりすることは明らかになっている程度「コンテキスト付き」での情報なわけで。まぁ私なんかは「お呼びではない」ので多くは語らぬけどね。この手の情報発信を放置するとしたらコミュニティって一体何なんだ?

# 個人的には楽しんでるけどね。

あとさ、公正な引用の可能性を言及しない批判がページに含まれているってどうなのさ。議論の拡散的にとか論旨的にこれは問題ないとの認識ですかどうですか偉い人。

コミュニティはちゃんと突っ込むんだ。そうだね、自由って素晴しい!


2007年09月04日

無断リンクを禁止する移民たち

無断リンク禁止は、WWW をどう考えているかの問題あたりらへん」を読んだ。

え、何? また「無断リンク禁止」が問題になってるの? これ、何周目の議論だろ。WWW日本語圏に限っても5周や6周じゃ済まないよね。ただ、私にとってはその繰り返しもまったくの無意味ではなかった。無断リンクを禁止したがる人々にとってのWWW、彼らが感じているWWWというものがいくらかは分かってきたのだ。彼らの「ほーむぺーじ」も「ぶろぐ」も「ぷろふ」も発信者個人のアイデンティティと極めて密接に結び付いた感情表現の集積であり、リンクが成す生態系それ自体も彼らの感情ネットワークの反映であり、そこに土足で暴力的なリンク(=感情)が割り込んできては困るのだ。

昔々、書いたことがあるけれども、これはどうやら文化の違いらしい。何を見るのか、何を感じるのか、思考の枠組、その違い。見ているものがちがうのだから、まずそこからすり合わせていかなければ対話は成立しない。そして、どちらが正しいというものでもない。

ただ、リンクを自由にしておきたい人々と、被リンク者の管理の下に置きたがる人々は対称ではない。前者は先住民で、後者は移民である。私は先住民の末裔の一人として移民たちに言いたいと思う。

私は、先住民の多くがそうであるように多様性を尊ぶ。故にあなたがたがWWWに植民してくることを歓迎する。だが、あなたがたが私たちを尊重しないなら私はあなたたちを尊重しない。偉大なる族長Tim Berners-LeeがHTTPを規定し、HTMLを設計し、www(httpd)を書いた。これによってWWWの大地が切り拓かれた。情報が自由を求めるという共通する価値観の下にFreesoftwareがあり、WWWの大地の豊かさにも支えられながらLinuxが育ち、花開いた。

自由の民がWWWの豊穣を作り出した。あなたがたが豊かな土地を求めてやってくるのは構わない。けれども、自由の民の文化をどうか殺さないで欲しい。自由の民の習慣を野蛮だと言って禁じることはしないでほしい。私もあなたがたの文化を尊重するように心がける。ゆくゆくは文化は交じり合い摩擦もなくなっていくだろう。その日が訪れるまで私は共存を望む。習慣の違いが互いに不快をもたらしたときには、穏やかに話し合おう。何よりも、互いに違いを知ることで不快を減らすことも可能なはずだ。共存を不可能にするような高圧的な態度をとらないで欲しい。私たちにも私たちの道徳があるのであり、あなたがただけが正義ではないと知ってほしい。

もし、あなたがたがどうしても共存できないようなやりかたを望むなら、その時は私はこう言わざるを得ないだろう。「よそ者は出て行け」と。そんな悲しいことは言わせないで欲しい。

Blog操作

検索


カテゴリー

このブログについて

あわせて読みたい

follow yugui at http://twitter.com

© Yugui

Powered by Movable Type 3.2-ja-2