コンピュータやソフトウェアのあれこれ@道民(&元道民)
hiratara
This user hasn't shared any biographical information
Homepage: http://d.hatena.ne.jp/hiratara/
Posts by hiratara
[perl+web]Tengのソースを読む(3)
9月 16th
概要
第一回で見たようにPluginはTengクラスにメソッドを生やすものなので、どんなメソッドが生えるか見る。
Teng::Plugin::BulkInsert
複数列を同時に投入する bulk_insert を提供。Kohadaから持ってきた処理ぽい。MySQLか否かで処理が分かれていて、MySQLだとSQL::Makerの InsertMulti プラグインを用いて1文で複数行をinsertする。その他のDBの場合はひたすらinsert。TRIGERに関するコメントが入っているが、多分昔の名残。
Teng::Plugin::Count
カウントするcountを提供。SQL::Makerでカウント文を作って流すだけ。
Teng::Plugin::FindOrCreate
find_or_create を提供。single して insert しているだけ。insert後のrefetch は冗長な気も?
Teng::Plugin::Pager、Teng::Plugin::Pager::MySQLFoundRows
ページャを意識した検索をする search_with_pager を提供。引数で指定したページに含まれるべき物件のみをLIMITで検索した結果リストと、 Data::Page::NoTotalEntries を返す。その割にはNoTotalEntries->first と NoTotalEntries->last は使ってなくて、ちょっと違和感がある。search 使えるんじゃないかって気もするけど、それも使ってなくて自前でSQL::Makerを使っている。
Teng::Plugin::Pager::MySQLFoundRows でも同じメソッドを実装しているが、こちらはData::Pageを返す。つまり、全行数も算出して返す。全行の算出には SQL_CALC_FOUND_ROWS を使うため、MySQL専用のプラグインとなっている。
Teng::Plugin::Replace
REPLACE INTO なSQLを実行するreplace を提供する。insertメソッド と同じ流れでREPLACE INTO にした感じなんだけど、差異があるのはその後の変更についていけてないせい?
*1:「> IT'S IN ALPHA QUALITY. IT MAY CHANGE THE API WITHOUT NOTICE.」
[perl+web]Tengのソースを読む(2)
9月 15th
Teng-0.11*1の続き。今日はSchemaクラスとRowクラス周り。
概要
Teng::Schemaと Teng::Schema::Table はスキーマ情報の保持、Teng::Schema::Declare とTeng::Schema::Loader, Teng::Schema::Dumper はスキーマ情報の作成、Teng::Rowと Teng::Iterator はDB内の行データの実態として振る舞う。
Teng::Schemaと Teng::Schema::Table
Teng::Schema はcamelize をユーティリティ関数として持つ以外は、ただスキーマデータを保管するだけのクラスである。サブクラス化して使われる(Loader使うと直接インスタンス化されるけど)。
Teng::Schema::Table はサブクラス化せずに使われるものだが、その配下にTeng::Row のサブクラスとなるRow クラスを持つ。コンストラクタではRow クラスの初期化も行っており、ユーザがカスタムクラスを作っていなければrow_class フィールドで指定した名前のクラスを勝手に作る。デフォルトでは各カラムのインフレーターを遅延で呼び出すようなクラスが作られる。
後、Teng::Schema::Table はデフレータとインフレータを呼び出すcall_deflate と call_inflate も提供する。デフレータとインフレータは直接カラム名を指定するのではなく正規表現を指定するので、xxxx_date というカラム名に対してまとめて1つのデフレータを指定したりもできる。(当たり前だが)1つのカラムへ同時に作用するデフレータは1個だけであるので、正規表現は被らないように指定する必要がある。
Teng::Schema を直接使ってスキーマも作れるが、DeclareやLoader, Dumper を使うともっと簡単である。
Teng::Schema::Declare
_current_schema ではcaller を遡って呼び元を探し、そいつをTeng::Schema のサブクラスにしてnew を呼んでインスタンスを生成する。インスタンスはシングルトンとして保存され、YourSchema->instance のようにアクセスできるように準備される。
ただし、schema 経由で _current_schema を呼ぶ場合はスタックは遡らずに 渡されたクラス名をTeng::Schema のサブクラスとする。これにより、package を作らなくてもスキーマクラスを作ることができる。
table 関数がDSLの入り口になっていて、name, pk, columns, row_class, inflate_rule はこの中でlocal 宣言される。これらの呼び出しは Teng::Schema::Table へ渡される。name に渡した値から自動的にRowクラスの名前が作られ、これは::Schema ではなく ::Row の名前空間にテーブル名をcamelize した名前が使われる。また、columns にはsql_types も同時に指定が可能で、{name => ..., type => ...} の形式で指定するとうまく動くっぽい*2。このtypeの 指定には DBI qw/:sql_types/ を使うとよいようだ。
DSLによって作られるTeng::Schema::Table は、全て_current_schema にaddされる。
Teng::Schema::Loader, Teng::Schema::Dumper
DBIx::Inspector の結果を、Loader は直接Schema オブジェクトにし、 Dumper は相当するPerlのソースコードを生成する。どちらもinflators とdeflators は生成されない*3。
Teng::Row
_lazy_get_data は各カラムのインフレータを遅延呼び出しする仕組みを提供する。_get_column_cached に結果を保存している。また、rowオブジェクトを使ってupdate をするときにSQL指定(スカラリファレンス)をするとオブジェクト側では正確な値が不明となるため、これを _untrusted_row_data で追跡していてこの値を取り出そうとすると警告が出る。
set_columnによるカラムの変更は _dirty_columns で追跡しており、update 時に変更されたカラムを登録する。update, delete, refetch はTengインスタンス側の物を使うので、違いと言えばupdate メソッドでカラムの変更を追跡するくらいのもの。
_where_cond はdelete や update 用のwhere句を作るメソッドで、ガッチガチに実装されている(そりゃ影響大きいのでそうか)。スキーマに主キーの情報がきちんと定義されており、データが主キーを全て含む場合のみwhere句を発行する。
AUTOLOADにて未知のメソッドはアクセサとして扱えるようにメソッドを生やすようになっている。また、AUTOLOAD除けのために空のDESTROYを定義している。
Teng::Iterator
select系のメソッドで使われる。渡された $sth に対して fetchrow_hashref しながらイテレーションして::Row を作っていくだけ。all は next 呼び続けるだけの素直な実装。
[レポート][haskell]今日は第2回スタートHaskell の日です
9月 11th
第2回スタートHaskellに来ていますので、レポートします。といっても、中身は全てProgramming in Haskell の話なのですが。
宿題の解説 / @yuzutechnology さん
宿題の答え合わせ。
- 問題2 : カリー化の問題。
- 問題3 : フレームレートやビットレートの計算。
- 非圧縮のサイズをレートなどから計算し、ファイルサイズと時間から単位をあわせて圧縮後のビットレートを出す。ただし、ビデオだけの計算なので音声の分を引く必要がある。
- 音声のビットレートは圧縮率なの? → 議論は後から
- fromIntegral ってのはわかるの? → 演習問題で出ていた。IntとFloatの型を合わせる。
- ある型から自型へ変換するのは fromXXXX、自分の型からある型へ変換するのは toXXX
- 型は合わせなければ成らないが、多相なのである程度だけ覚えておけば後は勝手にイイカンジの定義が使われる
- 問題4
- length が 4未満かの確認のコストは、長さによって変わるのでは? → その通り。長くなると不利。違う実装も考えられる
- かっこ要らないのでは? → hlint をcabalで入れておくとよい(インストールに3時間くらいかかる)。構文的に冗長な部分があればアドバイスをくれるっぽい?
- 問題5
- この問題は必要とされる数学の知識のレベルが高い。今回からは数学の問題は少なめになる。
- 各点が求められる平面上にあること、
- 点が時計回りに配置されること
- 外積ベクトルを出して同じ向きであること、辺のベクトルが外積と平行であること
- Haskell では where で式をたくさん並べられる。順番に実行されるわけでもなく、そこが面白い
第5章 リスト内包表記 / @dekosuke さん
- 前回 矢印 Prelude と 型クラス、多相型、カリー化、ガード、パターンマッチ
- なぜリスト操作が必要なの?
- 関数型には一般的 にはforループがない
- forやwhileはリスト、Iterator はFunctor
- リスト内包表記は、リストからリストへの演算。
- 無名関数+map+filterでも書ける。ケースバイケース。
- [x^2 | x <- [1..5 map の例
- [x | x <- [1..6], mod x 2 == 0] filter の例
- [(x, y) | x <- [1..3], y <- [x..3 複数の生成器を使う例
- リストを1つの値にまとめる関数(reduce(fold) など)とあわせて使うとよい
- length xs = sum [1 | _ <- xs]
- zip → 2つのリストを、1つのタプルのリストに
- 文字列の内包表記 → type String = [Char] なので
- take 2 "hoge" は "ho"。文字列のリストは、Char
- シーザー暗号を作って解読
- encode n "..." はn文字ずらす
- shift n c は、小文字だけをn文字ずらす
- encode n xs = [shift n x | x <- xs]
- table として英文でのアルファベットの頻度表を用意
- 25パターンのズラす量を全てχ2乗検定にかけ、誤差がもっとも低い物を採用すれば、シーザー暗号は解ける
- Haskell で内包表記はあまり使わないのでは? → プロジェクトオイラーとかでは使う。Pythonに比べるとあまり使わないイメージ
発表:第6章 再帰関数 / Lost_dogさん
- 再帰的定義 → 定義の中に自分自身が現れる
- 私の友達を「私の友達と話したことがある人」と定義してはいけない
- 循環を止める定義が必要「私。または私の友達と話したことのある人」
- "私"が基底部、"私の友達と話したことのある人"を再帰部と呼ぶ
- 数学的帰納法に似ている
- 定義の中に自分自身が現れる関数を、再帰的関数と呼ぶ
- frac 0 = 1; frac n = n * frac (n-1)
- 再帰的関数の作り方
- 1. 型を作る : product :: [Int] -> Int
- 2. 基底部と再帰部を作る product [] = ??? ; product (n:ns) = ???
- 3. 楽な方を先に実装(だいたいは基底部) product [] = 1
- 4. 大変な方を実装。productはもう動く物と思って作る product (n:ns) = n * product ns
- 補足
- RWHでは、先に再帰部を書き、下に基底部を書く
- マッチする回数の多い方が上の方が効率がいい? → GHCだと、基底部を先に書いた物としてコンパイルされる
- (RWH はスピードおたくが書いてるんだけど、その効果はなさそう)
- 再帰は再帰の度に型チェックがあるのがありがたい(なのでLISPで再帰だとちょっと意味が・・・)
- 最終的には再帰はなるべく書かない。foldやfilterやmapで力不足のときだけ。ただ、最初は練習した方がよい
- なぜ再帰は書かないのか → 意味が分かりにくい。fold などの方が意味はわかりやすく間違いも入りにくい
- Q. for がよくないってのはどういう話?
- for のbreakやcontinue で型チェックはしてない
- for の中でも、変数の型のチェックがあるのでは? → そこまで簡単だとそうだけど、複雑になると・・・
- 関数と文(for)を比較しているのが問題なのでは → 大事なのは、全てが"式"である、ということ
- for は構文であって副作用を期待しており、何を計算しているのかが不明確、ってことでは?
- OCaml とかでは、forの中にはinitを返す式しか書けなかったりもする
発表:再帰関数(補足) / トラビスさん
- factorial → 負の数を渡すと、無限ループ
- これは"部分関数"という。終了しないのは危険なので、error を使った方がいい
- n | n <= 0 = error "...negative"
- ケース文にて、頻度が高い物は上にした方がいい*1 → n > 0 を一番上
- Q. Haskell の仕様で、"ケース文を上から下にチェックする"という仕様が残っているのはなぜなの? 最適化の邪魔では?
- if 文のネストを書くときのようにケース文を使うには、これは必要
- 「と同じになるように」
- 本にあるreverse の実装は遅い
- haskellは連結リスト
- consは早い(リストは永続するので、残りの部分を再利用できる)
- tail も早い。
- 末尾に値を加えることはできない(永続性が崩れる)。この場合はコピーが必要。
- 本のreverse の定義の ++ [x] は毎回コピーするのでヤバい
- ヘルパ関数reverse' を定義する。acc(アキュミュレイター)という引数を増やす
- acc に結果をためていく。結果としてconsだけで実現できる
- reverse = reverse' [] where reverse' acc (x:xs) = reverse' (x:acc) xs ...
- Haskell は遅延評価のために、末尾再帰はすこし複雑。
- reverse' は末尾再帰にならない。評価が残っているので
- Q. アキュムレーターって何?
- A. 返したい値を少しずつ集めるもの。集めた後に、最後に変換することもある。
- A. 末尾再帰だと、結果を捨てられる。なるべく末尾再帰に直すのがよい。その解決手段として、アキュムレーターを追加することがよく行われる
- アキュムレータ(acc)を第一引数にすると、カリー化が使えてよい。
発表:第7章 高階関数 / @imagawa_yakata さん
- 公開関数とは関数を引数・戻り値とする関数。ここでは前者
- リストを処理する関数 : or , sum など
- 例えば、リストに10未満の数があるかを調べるには?
- filter even [1 .. 5] (evenが関数)
- map (+1) [1 .. 5] (+1 が関数)
- 10未満の偶数があるかをチェック → or $ map even $ filter (\n -> n < 10)
- 合成演算子 : 「.」 → f . g = \x -> f (g x)
- sum のような感じで、関数を集約できる関数はないの?
- foldl → foldl (+) 0 [1 .. 3] : 0がアキュムレーターとなっている
- foldl (.) id [or, map even, filter (\ n-> n < 10)] はできるのかな?
- できない。Listの中身の型がバラバラ
- 同じ方の関数ならできる
第2部(演習)
えんしゅうするっぽいです。
- import Char は非推奨。import Data.Char
- リストの変数名の末尾には「s」を置く
- elem は `elem` の中置きの表記で書く。バッククォートは結合が弱いので、括弧が要らなくなることがある
- Hoogle を使うとよい。作ってるのはhlint を作った人。
- seq や ! を使って正格評価させて、数値を潰してスタックを食いつぶさないようにする
- ghcではBangPatterns プラグまで、引数に!をつけるようにできる
- モジュールのあるパッケージを特定するいい方法がない(Hoogleを使う?)。コメントにbaseやmtlなどと書くとよい
- -Wall をつけて、警告は全て消すとよい
- ghc で -O -ddump-simpl をすると、中間コンパイルの状態を見れる
- ++ は左の要素のみをコピーする。よって、[..]++([..]++[..]) を ([..]++[..])++[..] と書くと、コピーの量はO(n)からO(n^2)へ増える。
*1:先のGHCでは変わらないという説はあるが、さっきのはパターンマッチの話。また、最適化などで変わるかも
[perl+web]Tengのソースを読む(1)
9月 8th
次はORMの最近の事情を知りたいのでTeng-0.11(ただし、「> IT'S IN ALPHA QUALITY. IT MAY CHANGE THE API WITHOUT NOTICE.」)。今日はTengクラス周り。
Tengでは Teng->new で得られるオブジェクトを起点に様々な操作を行う。DBやテーブルやカラムの情報を設定するスキーマクラスがデフォルトで「クラス名::Schema」となるようになってるので、Tengを継承してYourModel.pm を作っておくのがよい(そうすれば、YourModel::Schema にスキーマ情報を書ける)。スキーマは Class::Load::load_class を使ってロードされ、instance でインスタンスが作られ保持される。テーブルの情報はスキーマに書かれるので、DBからのロード時に各レコードに対応するオブジェクトを作るには schema->get_table して $tabel->row_class->new するのが基本。
コンストラクタでは他に、起動時のpidを保存している。これは$dbhがforkを跨いで利用されないように監視するため。また、DBへの接続がまだな場合は接続も行われる。このとき、TransactionManager が古い$dbh を握っているので、こいつも一旦破棄する。ここでon_connect_do フィールドにフックを仕掛けておくと、接続後に割り込めそうだが、ドキュメント化はされてないっぽい? on_connect_do にはSQLも直接仕掛けることができる。接続後に_prepare_from_dbh 内では接続後の初期化として Teng::QueryBuilder をDriverに合わせて作成する。Teng::QueryBuilder はまんまSQL::Maker 。
SQLの実行はほとんどの場合_execute メソッドを経由するが、ここでsql_comment パラメータ、またはTENG_SQL_COMMENT があるとスタックを辿って呼び出し元を特定し、SQLのコメントにその情報を追記する。これ、面白い。
insert は SQLMaker 使ってinsert した後、row_class->new して返す。ただし、主キーが指定されていればsingle でSELECT を発行して読み直す。それが鬱陶しければ fast_insert すれば余計なことは一切やらない。insert 時には_insert 内でschema->get_table の持っている情報を使って 値の deflate などを行う。なお、_last_insert_id では各RDBMSごとの差異を吸収しているが、Oracleだけ実装されてないようなので注意。
Teng クラスの持つupdate() やdelete() はバルクアップデートで、更新した件数を返す。do() はDBI->do のラッパーだが、0.11 の実装では_executeを経由しないのでTENG_SQL_COMMENT の恩恵は受けられない。row_class もこれらのメソッドを持つようだが、それは次回以降。
search(), search_named(), single() はsearch_by_sql() のラッパーという構成。single() はlimit => 1 を指定しているだけで実態はsearch()。search_by_sql() では$table の指定を省略された場合でもrow_class と紐づけを行うため、_guess_table_name でSQLからテーブル名を認識させている。ただ、from を拾うだけなのでサブクエリがあるSQLだとしくじるかも?
トランザクションはほぼDBIx::TransactionManager に丸投げ。connection() 時にトランザクション中だとまずいのでエラーを出すコードがあるのと、txn_scope でcaller を適切に詰め直す処理が追加されているくらい。txn_begin でcaller の詰め直しが要らないのかはちょっと疑問。
最後にpluginを読み込むためのload_plugin。+をつけないとTeng::Plugin:: が名前空間のベースになる。プラグインはExporter.pm は使ってないが、@EXPORT にエクスポートするメソッドを指定しておくとload_plugin メソッドがメソッドを生やしてくれる仕組み。load_pluginを経由せずにプラグインをuse しても意味ないし、プラグイン内で@EXPORT_OK などは使えない。
[perl+web]DBIx::TransactionManagerのソースを読む
9月 5th
今日はDBIx::TransactionManager-1.09。1ファイルしかないのでさっくりと。
中では2クラス定義がされていて、DBIx::TransactionManager と DBIx::TransactionManager::ScopeGuard。前者がメインで、後者はスコープガード用のクラス。
txn_begin の中では$dbh->begin_work をしていて、こいつは$dbh->{AutoCommit}を一度無効にして、トランザクションが終わったら自動で$dbh->{AutoCommit} を有効にしてくれるという優れもの。なんだけど、begin_work はネストして呼べないのでそれを可能にするのが TransactionManager のお仕事。active_transactions フィールドがミソになっていて、txn_begin されるごとにここへcaller の情報を貯めていく。このフィールドを見ればネストしているか分かるので、ネストしている場合はbegin_work はしない。
txn_begin へcaller を外から引数として渡せるようになっているのは、TransactionManager のラッパーを作る場合を考慮してのこと。ラッパーを作るときは正しい呼び出し元をcaller パラメータへ渡すようにするとよい。
txn_commit は、ネストの内側で呼ぶと何もしない。ネストの一番外で呼んだときのみ$dbh->commit をする。ネストの途中で txn_rollbackされていると、エラーで落ちてしまう*1。同様にtxn_rollback で実際にrollback が走ってくれるのは、ネストの一番外で呼んだときのみ。
::ScopeGuard は基本的にTransactionManager へ処理を委譲する。が、各スコープに対して2度以上commit やrollback をするとtxn_begin の呼び出し回数と矛盾してしまうので、一度しか効果を及ぼさないようにフラグで管理している。rollback も commit もしないでインスタンスが解放される場合には、DESTROY 内でwarning を出しつつ txn_rollback している。
*1:ネストの深ーいとこで誰かがrollback した場合に、一番外側でcommit ではなくrollback を呼ぶ必要があると思うんだけど、commit しちゃあイカンのだってことをどうやって知るのがいいんだろう。
[perl+web]Amon2のソースを読む(5)
8月 29th
今日はAmon2-2.49のDSL周り。これで最後。
Amon2::Declare
$c じゃなくて c() 使えたらいいよねって話。内容物の中では、t/ 内のExtended::V::MT::Context にて、テンプレからcを呼び出せるようにするために使っている。
Amon2::Lite
Sinatra-ish な書き方ができるようにするもの。Amon2::Lite::_child_N という名前でコンテクストクラスを作る。Nは連番なので、package名さえ分ければ複数箇所から利用が可能*1。
use 元にはルーティングに必要な関数をエクスポートする。2.49 の実装では Router::Simple::Sinatraish は使わずに自前で実装している。さらにコンテキストにはdispatch と create_view が必須なのでこれらを実装。create_view では__DATA__ セクションを読むために初回呼び出し時までテンプレのロードを遅らせている。このときにuse 側のくらすにcreate_view メソッドを生やし、ロードが複数回走らないようにしている。デフォルトで作成されるビューは TT2Like。また、Liteではconfigディレクトリが無い可能性があるので、config/ ディレクトリが無くても落ちることがないようにload_config() メソッドを上書きしている。
その他
create_view で作成されるテンプレートにはrender メソッドが要求されるため、Xslate 以外のテンプレートを使うときにちょっと困るが、その場合はTiffany を経由させるとよい。その辺はLite の PODや t/apps/Extended のExtended::Web 辺りを見ればわかる。
*1:Plack::Util::load_psgi を使えばpackageは自動的にサンドボックスの中になる
[perl+web]Amon2のソースを読む(4)
8月 28th
今日はAmon2-2.49のTriggerとPluggin。
Amon2::Trigger
クラスに対してもインスタンスに対してもフックがかけられ、それぞれ@Class::_trigger と $obj->{_trigger} を使って保存される。get_trigger_code を呼ぶと関連するハンドラを全て取得できるが、インスタンスに対して呼んだ場合はインスタンスに紐づくハンドラとそのクラスの継承ツリー上のクラスにぶら下がる全てのハンドラを得られる。ハンドラは親→子の順に並んでいる。厳密な順序に関しては、mro::get_linear_isa を使っているので現在設定されているmroの影響を受ける。
Amon2::Plugin::Web::
プラグインは $c->add_trigger でトリガを仕掛けたり、$c にメソッドを生やしたりしてYourProject::Web クラスを拡張する。
Web::FillInFormLite は$cにfillin_form メソッドを生やしてくれる。このメソッドに値をセットしておくとHTML_FILTER トリガーでフォームに値を埋めてくれる。
Amon2::Plugin::Web::NoCache はPragma とCache-Control ヘッダを設定するもので、AFTER_DISPATCH トリガーで処理される。
CSRFDefender はCSRF防止用のプラグインで、Sessionプラグインとの併用が前提*1。HTML_FILTER トリガーで<form>を見つけたら csrf_token というhiddenのinput要素を埋め込み、ここにセッションに保存されているトークンを埋め込む。BEFORE_DISPATCH トリガー内でPOSTされてきた*2トークンとセッション内のトークンを比較し、異なる場合は403ステータスで落とす。または手動で管理したい場合は、load_plugin 時の設定で no_validate_hook => 1 を渡してPOST時の自動チェックを抑止し、$cにget_csrf_defender_token と validate_csrf が生えているのでこれを使う。
HTTPSession は$cにsession() メソッドを生やし、こいつを呼ぶ度にHTTP::Session->new してインスタンスを作る。作られたインスタンスは$c->{'HTTP::Session'} に保管される。AFTER_DISPATCH トリガーで最後にresponse_filter と finalize する。HTTP::Session::Store:: と HTTP::Session::State:: はload_plugin 時に指定しておく必要がある。
PlackSessionとJSON はscaffold のコードではロードされない。PlackSessionは$cにsessionメソッドを生やして処理はPlack::Session にお任せ。
JSON は$c にrender_json を生やしてくれる。長いように見えるが、やっていることはブラウザごとのバットノウハウとセキュリティ絡みの考慮。Chrome表示の振り分けに利用されている X-Requested-With はjQuery などのフレームワークがセットする物なので、素のXMLHttpRequest を使う場合はこのヘッダをセットしてリクエストするようにしたほうがよい。
[perl+web]Amon2のソースを読む(3)
8月 26th
今日はAmon2-2.49のscaffoldとDispatcher。--flavor=Basic が基本なんで、それを読む。
概要
scaffold 内のPerl関連のファイルはapp.psgiとYourProject.pmとYourProject::Webと YourProject::Web::Dispatcher だけ。実際のユーザ側の処理は YourProject::Web::Dispatcher がディスパッチした先に書くことになるので、Dispatcher.pm に直接書くか、YourProject::Web::C 以下のクラスに書くことになる。
YourProject::Web::Dispatcher を書くために Amon2::Web::Dispatcher::Lite と Amon2::Web::Dispatcher::RouterSimple が提供されているので、これも一緒に見る。
なお、 YourProject::Web::Dispatcher を使う気がないなら、Amon2::Web->dispatch に直接処理を書くこともできる。そうすると、--flavor=Minimumの構成となる。
app.psgi
YourProject/app.psgi では、robot.txtとfavicon.icoと/static/ をPlack::Middleware::Static へ渡して静的に処理させている。また、Plack::Middleware::ReverseProxy が入っていることからリバースプロクシの裏での動作を前提としていることがわかる。必要に応じてenable_if でReverseProxyを有効にするアクセス元のホストを指定するように変更するのもよいだろう。
YourProject.pm
YourProject クラスはAmon2 クラスを継承したコンテキスト。WEBに特化しない処理はここに書くとよい。例えば、DBIプラグインのロードとか。
YourProject::Web
YourProject::Web ではYourProject クラスを継承してコンテキストとして振る舞うとともに、Amon2::Web をMixinとして取り込んでWebの機能を提供する。このクラスの最も重要な仕事は Amon2::Web が要求するdispatch と create_view を実装すること。create_view は初期化したXslate を返すように実装されており、テンプレから c という変数でカレントコンテキストにアクセスできるようにしている。他、テンプレへはuri_with とuri_for のショートカットも提供。dispatch メソッドの実装はYourProject::Web::Dispatcher->dispatch に投げるだけになっている。
YourProject::Webにはその他に、YourProject::Web::C:: にあるコントローラを全部ロードしたり、プラグインを読み込むコード、フックの定義などが含まれる(が、デフォルトのscaffoldだとAmon2::Web::Dispatcher::Lite を使ってるので、C::以下のクラスを定義する必要はないように思える)。
YourProject::Web::Dispatcher
デフォルトではAmon2::Web::Dispatcher::Lite を使ってディスパッチするコードが書かれている。eg/ ディレクトリには Amon2::Web::Dispatcher::RouterSimple を使った例が入っている。::Liteも::RouterSimple も定義したルーティングに対して __CLASS__->dispatch($c) というコンテキスト$cを受け取るメソッドを作ってくれるので、scaffoldでYourProject::Web 内に生成されるコードと相性がいい(そういう風にscaffoldが作られてるのだから当たり前だけど)。
Amon2::Web::Dispatcher::Lite
use Router::Simple::Sinatraish するのと同じになるよう、import 関数内で Sinatraish->export_to_level(1) で関数類をエクスポートしている。追加でコンテキスト$cとルーティング情報を結びつける糊のコードとして、前述のdispatch メソッドを定義している。この糊は、 $envに対してmatchを実行して、選択された処理を$cとマッチング結果を引数として呼び出す。URL内にパターンを指定したなら、この第二引数のマッチング結果から参照できる。
Amon2::Web::Dispatcher::RouterSimple
このコミットの辺りからこのクラスは表立っては使われなくなったので、あまり推奨しない方向に進んでるのかもしれないけど一応読む。
Router::Simple をDSL的に使えるよう、connect、submapper、match、as_stringを関数としてエクスポートしている。これらの処理の主役となるインスタンスはrouter関数で取得できる。connectはRouter::Simpleのものより少し拡張されていて、"コントローラ名#アクション名"という指定もできる。この指定の場合の \%destination は、{controller => コントローラ名, action => アクション名}と解釈される。
dispatch がコンテキスト$cとマッピングを結ぶ糊で、"コンテキストクラス名::C::コントローラ名"というクラスのアクション名のメソッドを呼ぶ。コンテキストクラスの名前は、通常はYourProject::Web となるはず。PODにも書いてあるが、コントローラ名とアクション名はmatch()結果のcontrollerとactionを使うので、パターンに:matchや:controllerを書いておくとURLを元に自由に指定させられる(使いどころは少ない気がするけど)。
ディスパッチされたメソッドには、Liteの時と同じようにコンテキスト$cとmatch()の結果が渡されるので、これを使って処理を書く。
[perl+web]Amon2のソースを読む(2)
8月 25th
今日はAmon2-2.49のコンテキスト周りで、具体的には Amon2、Amon2::Web、Amon2::WebRequest、Amon2::WebResponse、ついでにAmon2::Config::Simple 。Dispatcher はこれらと粗結合なので、次回以降。他残っているのは、scaffoldのコードと、トリガーとプラグイン周り。
Amon2::Web と Amon2 の関係
Amon2::Web はAmon2(コンテキスト) を継承しておらず、コンストラクタも持っていない。YourProject::Web ではAmon2 と Amon2::Web の双方を継承して使う。という構造を考えると、Amon2::Web はAmon2 のサブクラスに対する Mix-in として働いていると見た方がわかりやすい。
Amon2::Web
scaffoldの YourProject/app.psgi内では YourProject::Web 内のto_app を呼んでPSGIアプリケーションを作成しているが、このメソッドの定義元はAmon2::Webであり、Amon2::Web がPSGIリクエストを処理する際のエントリポイントになっている。
生成されるPSGIアプリケーションは単純な物で、Amon2::Web::Request を作ってYourProject::Web->new をして、dispatch() を呼び出すだけ。ここで、Amon2::Web は1リクエスト1インスタンスのモデルを取っていることがわかる。dispatchを呼ぶ前後には BEFORE_DISPATCH と AFTER_DISPATCH という二つのトリガーがあり、呼ばれる。BEFORE_DISPATCH や dispatch() から返すレスポンスはPlack::Response のサブクラスを想定しており、Amon2::Web::Response もその1つとなっている。
YourProject::Web 内からレスポンスを返す方法として、Amon2::Web はredirect、res_404、renderの3つの方法を提供している。res_404 が最も単純で、404 Not Foundの画面を返すもの。これはAmon2::Web::Dispatcher:: 以下のクラスでも使っている。htmlもres_404 メソッドの中で定義されているので、404画面を置き換えたければYourProject::Web 内でこのメソッドをオーバーライドするのがいいと思われる。redirect はQUERY_STRING の扱いのためにコードは長いが302のレスポンスを返す以外の特別なことはしてない。render はcreate_view->render するだけ。create_view はYourProject::Web で自分で定義する必要がある(scaffoldに入っている)。HTML_FILTER のトリガはここで処理されるので、render 以外の方法でレスポンスを返すとHTML_FILTER トリガは呼ばれない。
Amon2::Web のやってることはこれだけ。リクエストをルーティングしたいときはdispatchメソッド内でAmon2::Web::Dispatcher:: へ処理を委譲する。その他の処理はプラグインをロードすることで実現する。その辺はscaffold にもデフォルトで書いてあるので、必要に応じていじくるとよい。
あと、Amon2::Web 内のencoding はhtmlやURLに含めるパラメータのエンコーディングを決めているので、UTF-8以外の文字コードを使いたければ encoding と html_content_type のメソッドをオーバーロードする。
Amon2::Web::Request, Response
Amon2::Response はマンマ Plack::Response。Amon2::Request はがっつりコードがあるように見えるが、これはparameters 系のメソッドを(バイト列ではなく)文字列を返すようにしているだけ。Plack::Request と同じ値が欲しければ、parameters_raw などを用いる。
ところで、Amon2::Web->url_for と Amon2::Web::Request->url_with の二つが存在してこれらがテンプレから使えるように YourProject::Web で設定されるが、これらは全く別もの。前者はベースURLを元にURLを作る物で、このアプリがルートにデプロイされない場合はこのメソッドを通してURLを作る必要がある。後者は現在のリクエストを元にQUERY_STRINGの部分だけ置き換え(もしくは追加した)リクエストを作る物で、Catalystに由来する。
Amon2
最後に、Amon2 クラスだが、これはコンテキストを表すクラス。言い換えれば、CLIでも使える機能をAmon2::Webから分離した物とも言え、プラグインのロードやコンフィグへのアクセスなどの機能を提供する。Amon2->context で現在のコンテキストを取得可能であり、WEBアプリだとYourProject::Web のインスタンスであり、CLIアプリだとAmon2クラス自身のインスタンスが入っている。
コンフィグ周りはAmon2::Config::Simple が面倒を見ているが、こいつはAmon2->mode_name と同じ名前の*.pl をdo して返す。この振る舞いが気に食わなければAmon2->mode_name とか Amon2->load_config をオーバーライドして動きを変えればよい。
load_plugin は指定されたプラグインをrequire してパラメータ(hash-ref)付きでinit() を呼ぶ。デフォルトでは Amon2::Plugin 以下のモジュール、+付きだと任意のパッケージ名のモジュールをロードするが、この仕事をしてるのは2.49の実装だとPlack::Util。プラグインはinit()メソッド内でコンテキストにメソッドを追加したりトリガを仕掛けたりして動作を拡張する。
load_plugins は一度に複数をload_plugin できる。Data::OptList::mkopt を噛ませてるお陰で、モジュール名とパラメータを混ぜた形式で指定ができる。
# YourProject::Web より __PACKAGE__->load_plugins( 'Web::FillInFormLite', 'Web::NoCache', # do not cache the dynamic content by default 'Web::CSRFDefender', 'Web::HTTPSession' => { state => 'Cookie', store => HTTP::Session::Store::File->new( dir => File::Spec->tmpdir(), ) }, );
ちなみに、base_dir と config は、2度目以降の呼び出しを高速にするために、初回実行時にサブクラス側で同名のメソッドを生やし、オーバーライドして自身を隠している。
[perl+web]YAPC::Asia Tokyo 2011 「チケット絶賛発売中です!」とのこと
8月 25th
YAPC::Asia Tokyo 2011 が10/13〜10/15 に開催されます。チケットの販売も始まってますので、買ってない方は締め切りが近いようなので早く買った方がいいです。
今年は初スピーカーとして出られることになったので、Monads in Perlという題でこんな話とかこんな話をAE::cv を例にとって話すつもりです。AE::cvは Stateモナドより断然簡単なんで、JSDeferred がわかるレベルの人であれば理解できると思います*1。
*1:というか、先にネタバレさせておくとほぼマンマJSDeferredになるんですが