モダンPerlの世界へようこそ

第9回Jifty:一足早いクリスマスプレゼント

Perl 6チームからのクリスマスプレゼント

この連載でも何度か名前が出てきたPerl 6ですが、⁠クリスマスまでにはリリースされることになっている」という話はみなさん先刻ご承知のことと思います。

もちろんこの話にはどのクリスマスかは秘密ですというオチがつくわけですが、2000年の設計開始以降、これまでにも何度か「今年こそはひょっとするとひょっとするかも」という期待を持たれた年がありました。

オードリー・タン(唐鳳)氏がHaskellの勉強がてらわずか2ヶ月でPerl 6の処理系Pugsを実装して話題になった2005年は、まさにその筆頭格といってよいでしょう。

実際、この年はPerl 6とその関係者にとって非常に大きな意味を持つ年になったのですが、この年はまたRuby on Railsが本格的にブームになった年でもありました。ただでさえPerlからRubyに転向する人が後をたたなかったというのに、⁠クリスマスプレゼント」のひとつもないようでは、ますますPerl離れが加速するおそれがあったわけです。

この年前任のアリソン・ランダル(Allison Randal)氏からPerl 6のプロジェクトマネージャーの職を引き継いだジェシー・ヴィンセント氏は、世の父親がよくするように、それとなく「クリスマスプレゼント」の希望を聞いてまわりました。なかには「お父さん、私ポニーを飼いたいの!」という現実離れした要望もありましたが、そのような無茶振りにもなんとか応えてしまうのがドラえもんもといPerl使いの真骨頂。

残念ながらPerl 6そのものではありませんでしたが、その年のクリスマスに、氏は「ごちゃごちゃとデータベースをいじり回す必要のない」⁠Ajaxに対応した」⁠クリーンでシンプルな」⁠CPANを活用した」⁠バズワード満載の」クリスマスプレゼントを公開します。

それが、Jiftyというウェブアプリケーションフレームワークの始まりでした。

どこまで本当の言語らしく書けるか

Maypoleという既存の成功したフレームワークからユーザを引き継いだCatalystに比べると、ほとんどまっさらなところから生まれたJiftyは最初からそれほど多くのユーザを引きつけたわけではありません。

が、Jiftyのドメイン特化言語(DSL)を多用した宣言的なプログラミングは、モダンPerlの世界に大きな衝撃を与えました。翌2006年春に登場したMooseがいまのような形になった背景には、あきらかにJiftyの影響が見られます。

2007年のYAPC::Asiaでも紹介されていましたが[1]⁠、Jiftyのドメイン特化言語は、Mooseの世界で見られるような「わかりやすい名前がついている(だけの、ふつうの)関数」ではありません。ソースフィルタリング以外のありとあらゆるPerlの黒魔術を駆使して、なるべく自然言語に近い書き方ができるように工夫されています。

その背景にある技術についてはまた次回以降取り上げますが、今回はまず、筆者が2006年にまるごとPerl! Vol.1にJiftyの入門記事を寄稿したあとでJiftyの世界で起こった変化をふまえながら、TwitterWassrのようなサンプルアプリケーションをひとつつくってみましょう。コードはgithubに置いておきますので、必要ならcloneしてお使いください。

環境を構築する

ご多分に洩れず、Jiftyも多くのモジュールに依存しているので環境によってはインストールに苦労するかもしれません。Windowsユーザの方は筆者が管理しているPPMリポジトリなどを駆使して環境を構築してください。また、Jiftyのリポジトリから最新版をチェックアウトしたい方は下記のコマンドを実行してください。

> svn co http://svn.jifty.org/svn/jifty.org/jifty/trunk

Jiftyのインストールが済んだら、適当な作業ディレクトリでひな形をつくりましょう。

>  jifty app --name MyApp

新しくできたMyAppディレクトリのなかには、設定ファイルや管理用のスクリプトなどが用意されています。

ログインまわりはプラグインを使えば簡単

今回は自分だけではなく、ほかのユーザも使えるものを目指したいので、Jiftyに同梱されている認証用のプラグインを利用することにしましょう。エディタでこのような内容のetc/site_config.ymlを用意してください。

framework:
  Database:
    Version: 0.0.1
  Plugins:
    - LetMe: {}
    - User: {}
    - Authentication::Password:
        login_by: username

内容については直感的にわかると思いますので特に説明しませんが、CPAN版のAuthentication::Passwordプラグインにはバグがあるので、そちらを使っている方はlogin_byの値をemailに変えてください(リポジトリのほうでは修正済みです⁠⁠。

続いて、シェルからこのようなコマンドを実行します。

> jifty model --name User

lib/MyApp/Model/User.pmにひな形ができるので、エディタで下のほうに次の3行を追加してください。最後の sub check_read_rights { 1 } は「このモデルのデータはだれでも読み取り可能」という意味です。

use Jifty::Plugin::User::Mixin::Model::User;
use Jifty::Plugin::Authentication::Password::Mixin::Model::User;
sub check_read_rights { 1 }

この段階で一度サーバを起動してみましょう。

> jifty server --start

これで自動的にデータベースの設定が行われて、⁠サインアップ」「ログイン」というメニューが追加された初期画面にアクセスできるようになります。

モデルを追加しよう

続いて、⁠ひとこと」を保存するモデルを用意しましょう。先ほどと同様に、シェルからヘルパースクリプトを実行します。

> jifty model --name Entry

Userモデルのほうはプラグインのほうで用意してくれた出来合いのスキーマを使いましたが、今度は自前でスキーマを用意します。MyApp::Recordのschemaブロックにこのようなカラムを定義してください。

use MyApp::Record schema {
  column body =>
    type is 'text';

  column user_id =>
    type is 'integer',
    refers_to MyApp::Model::User;

  column epoch =>
    type is 'integer',
    default is defer { time() };
};

一見裸のワードに見える単語がいくつも並んでいますが、この「is」はれっきとしたPerlの関数です(このような連結詞の存在がJifty的なDSLの顕著な特徴です⁠⁠。

refers_toはhas_aのような外部参照を指定するもの(ここでは「ひとこと」とユーザは一対一対応するのでMyApp::Model::Userという「レコード」を参照するようになっていますが、has_manyを実現したい場合は「refers_to MyApp::Model::UserCollection」のように「コレクション」を参照させます⁠⁠。deferは、Scalar::Deferというモジュールに由来する遅延評価のための関数です。

複雑なアクセスコントロールを実装する

このモデルは、だれもが好き勝手に更新できては困ります。データの読み取りは全員に認めますが、更新系の処理は自分の発言に対してのみできるようにしたいところです。

このような複雑なアクセスコントロールは、current_user_canというメソッドで行います。詳しくはJifty::Recordをご覧いただくとして、ここではread以外の処理はユーザIDが一致したときのみ実行可能にしています。

sub current_user_can {
  my ($self, $right, %args) = @_;

  return 1 if $right eq 'read';
  return 1 if $args{user_id} && $self->current_user->id == $args{user_id};
  return 1 if $self->user_id && $self->current_user->id == $self->user_id;

  return $self->SUPER::current_user_can($right, %args);
}

スキーマを更新しよう

モデルの処理としてはこれでひとまず完成ですが、このままではサーバを再起動してもデータベースの内容は更新されません。スキーマを更新するには、⁠1)新しいモデルに、どのバージョンのデータベースから有効になるか指定する、⁠2)設定ファイル内のデータベースのバージョンを更新する、⁠3)スキーマをセットアップする、という手順を踏む必要があります。

まずはlib/MyApp/Model/Entry.pmの最後にデータベースのバージョンを返すサブルーチンを用意します。

sub { '0.0.2' }

続いてetc/site_config.ymlの該当の場所に、新しいデータベースのバージョンを書き ます。

  Database:
    Version: 0.0.2

それが済んだら、ヘルパースクリプトを利用してスキーマを更新しましょう。

> jifty schema --setup

これで新しいモデルがデータベースに登録されました。もちろん生のSQLを書く必要はありません。

ディスパッチャもDSLで

モデルができたところで、今度はそれを表示するためのビューとディスパッチャの実装にかかりましょう。ビューやディスパッチャには便利なヘルパーは用意されていないので、自分で実装していきます。

エディタでこのようなlib/MyApp/Dispatcher.pmを用意してください。

package MyApp::Dispatcher;

use strict;
use warnings;
use Jifty::Dispatcher -base;

under 'user/*' => [
  run {
    my ($name) = ($1);
    my $user = MyApp::Model::User->load_by_cols(name => $name);
    set user => $user;
  },
  on qr/(\d+)/ => run {
    my ($epoch) = ($1);
    my $user = get('user');
    my $entry = MyApp::Model::Entry->load_by_cols(
      user_id  => $user->id,
      epoch    => $epoch,
    );
    set entry => $entry;
    show '/entry';
  },
  on '' => show '/list',
];

1;

ここではhttp://localhost:8888/user/<username>のようなアドレスにアクセスしたらそのユーザのひとこと一覧を、http://localhost:8888/user/<username>/<epoch>にアクセスしたら、そのときそのユーザがつぶやいたひとことを表示させようとしています。詳しくはJifty::Dispatcherをご覧いただくとして、ここではCatalystのチェーンドアクションと同じように、段階ごとに適切な処理を行うようになっています。

なお、ディスパッチャのなかでモデルにアクセスしているのは、将来ユーザやひとことが存在していなかった場合にエラー画面に飛ばすためです。getやsetは、Catalystでいうstashのデータを読み書きするためのもの。ビューではここでstashに登録したデータを使って必要な画面を作成していきます。

ビューもDSLで

続いてビューを作成しましょう。エディタでこのようなlib/MyApp/View.pmを作成してください。テンプレートには、当初はHTML::Masonが使われていましたが、いまはTemplate::Declareを利用するのが主流になっています。

package MyApp::View;

use strict;
use warnings;
use Jifty::View::Declare -base;
use Time::Piece;

template 'list' => page {
  my $user = get('user');

  if (Jifty->web->current_user->id == $user->id) {
    h1 { "Your tweets" }
    show 'form';
  }
  else {
    h1 { "Tweets by $name..." }
  }

  my $entries = MyApp::Model::EntryCollection->new;

  $entries->order_by( column => 'epoch', order => 'DESC' );
  $entries->rows_per_page(15);
  $entries->limit( column => 'user_id', value => $user->id );

  while (my $entry = $entries->next) {
    show('_entry' => $entry);
  }
};

template 'entry' => page {
  my $entry = get('entry');

  show('_entry' => $entry);
};

設定がメインのJiftyのDSLとは違って、こちらは出力がメインなので連結詞は使われていませんが、注意深い方なら当然セミコロンがあるべきはずのいくつかの場所にセミコロンがないことに気がつかれたかもしれません。かならずしもすべてのセミコロンを省略できるわけではありませんし、特にJavaScriptが絡むところでセミコロンを省略すると思いがけないバグに遭遇する可能性も高くなるのですが、これもTemplate::Declareの魔法のひとつです。

private template '_entry' => sub {
  my ($self, $entry) = @_;

  div { $entry->body }
  p {
    my $time = localtime($entry->epoch);
    span { outs "by " . $entry->user->name .
                " at " . $time->date . ' ' . $time->time }
    span {
      my $url = join '/', "/user", 
                          $entry->user->name,
                          $entry->epoch;
      hyperlink( url => $url, label => 'permalink' );
    }
  }
};

一覧と個別のエントリで共通に使える小さな部品はプライベートテンプレートとしてくくりだしておきました。$entry->user->nameの部分では外部キーによる参照を行っています。

private template 'form' => sub {
  my $user = get('user');
  my $name = $user->name;

  my $action = new_action(
    class   => 'MyApp::Action::CreateEntry',
    moniker => 'create_entry',
  );

  div { attr { class => 'Form' }
    Jifty->web->form->start(submit_to => "/user/$name/post");
    div {
      render_param( $action => 'body', focus => 1 );
      Jifty->web->return(
        submit    => $action,
        label     => 'Tweet',
        as_button => 1,
      );
    };
    Jifty->web->form->end;
  }
};

1;

Jiftyのフォームはふつう対応するアクションと密接に結びついています。データベースのCRUDや検索はすべて専用のアクションを用意するのがJiftyの流儀です[2]⁠。

Jiftyのアクション

ここではとりあえず作成用のアクションだけ用意しましょう。シェルから次のコマンドを実行してください。Entryモデルにひもづけられたアクションのひな形が用意されます。

> jifty action --name CreateEntry

アクションにはモデルの定義によく似たパラメータの定義を用意できます。monikerはこのアクションの識別名です(省略すると自動生成されますが、このように明示的にアクションを用意する場合は省略しないほうが無難のようです⁠⁠。

use Jifty::Action schema {
  param body =>
    label is '',
    type is 'text',
    display_length is 40,
    max_length is 255,
    sticky is 0;
};

sub moniker { 'create_entry' }

実際のアクションの中身はtake_actionというメソッドの中に書きます。ここではエントリを作成したあと、/homeにリダイレクトさせています。

sub take_action {
  my $self = shift;

  my $entry = MyApp::Model::Entry->new;
  $entry->create(
    body    => $self->argument_value('body'),
    epoch   => time,
    user_id => $self->current_user->id,
  );

  Jifty->web->next_page("/home");

  $self->report_success if not $self->result->failure;
  return 1;
}

これでひとまず動くアプリケーションができました。サーバを起動して、JavaScriptのオンオフを切り替えたりしながらアプリケーションの挙動を確かめてみてください。

単純に比較するのは間違っていますが

なるべく本体は軽くしてCPANモジュールの触媒、⁠のり」に徹することを目指したCatalystと、インストールしたらすぐに使える便利なフレームワークを目指したJiftyを、そのままの状態で比較するのは不公平というものでしょう。

でも、いくらCatalystに定番のコンポーネントを追加したところで(それどころか、前回取り上げたReactionを追加してさえ⁠⁠、Jiftyが(あるいはRuby on Railsや、Rails以降の各種フレームワークが)標準で提供している使い勝手には届かないのも厳然たる事実です。

もちろんCatalystにはCatalystの長所がありますし、JiftyにはJiftyの短所もあるのですが、もし身近に「PerlのフレームワークなんてWeb 1.0的なものばかりじゃないか」とお嘆きの方がいるようでしたら、ぜひJiftyをすすめてみてください。

国内ではあまり大きな採用実績はありませんが、海外ではJiftyの開発元であるBest Practical Solutions社が展開するHiveminderというサービスのほか、CPANのバグトラッキングシステムとして採用されているRTの次期バージョンがJiftyベースで書き直されて、いま最後の調整段階に入っているところです。

おすすめ記事

記事・ニュース一覧