D-7 <altijd in beweging>

Day to day life of a Perl/Go/C/C++/whatever hacker. May include anything from tech, food, and family.

カテゴリ:開発

週末にsupervisorっていうものについて話してる人がいたので、特に理由なくPerlでクローンを書いてみようと思った。というわけで本当に基本の部分まで書いた。名前はBrahman。神様の名前にしたかったのはプロセスの生死を管理するから。

繰り返すけど、特に理由はない。書きたかったから。ちなみに内部的には多分本家と全然互換性はない。設定ファイルがだいたいクローンできればいいや、というくらい。


今のところできることは、例えば今daemontoolsで管理してるplakupサーバーがあったとして、こんな感じの設定を書くと立ち上がってstdout/stderrを一緒のログに保存してくれる、ってところまで。

[program:yourapp]
environment=PORT=50000
directory=/service/yourapp
command=/service/yourapp/run
stdout_logfile=/var/log/yourapp.log
redirect_stderr=true

以上を書いて、brahmand -c config.ini とか書くと勝手にこのプロセスがずっと生きてるように管理してくれるでござる。brachmanctl list とかすると、現在動いているプロセスとかでてくるので、supervisor.processes.pid あたりをkillしてみると生き返るのが見えるかと思います。

一応頑張った点としては、万が一 ログのプロセスが死んじゃったとしても、daemontoolsのsuperviseに相当するプロセスが生き残ってればまたログを取るプロセスが生き返って、もう一度アタッチしてログをとり続けられる事と、ログを取る以外のプロセスもアタッチすることによってイベントを受け付ける事ができる、など。

あと外部からのコマンドうけつけはJSONRPCで行う。今はごめん、list(動いているプロセスとかをリスト)とstop(brahmandと管理してるプロセスをストップ)しかできないけど、しこしこ書いていけばいくらでもAPIは追加できる。

ここまで正味二日間でできた。JSONRPCは以前書いた物の流用、あとはTwiggyとかを以前からバリバリ書いてたおかげで楽だった。Twiggyがそのままエンベッドできるのにも助かった。

とりあえず自分の使ってる開発サーバーはこいつに移行できるようにしてみようかな、と思ってるけど、人柱とか一緒にやってくれる人がいると嬉しいなー、と。

    このエントリーをはてなブックマークに追加 mixiチェック

「オブジェクト指向なパラダイムでプログラムを書くとき」にClass::Data::Inheritableは排除すべきモジュールである。今回激しくそれを痛感しているので、だらだら書いてみたい。

まず、Perlはマルチパラダイムが可能な言語なので、Class::Data::Inheritable自体は否定されるべきものでもないし、あと必ず例外ケースはでてくるのでその際には躊躇なく使えばいいと思う。以下は最初の一文の通り、Perlでオブジェクト指向を使う場合はClass::Data::Inheritableは基本的に使わず、あくまで例外ケースに留めるべきだ、という事を伝えたい。

まずその1: クラスアトリビュートはグローバル変数

クラスアトリビュートはグローバル変数です。異論は認めません。

Singletonもそうだけど・・・使う場所はあるんだけど、グローバル変数は限りなく限定的に使われるべき。基本的に使わない物なので、確実にインスタンス変数では無理、もしくはその方がパフォーマンスが○○%向上する!みたいな箇所以外では原則使うべきものではない。

特にオブジェクト指向とは混ぜるな危険!ざっとググった中で一番簡潔に書いてあったのはこのブログかな

2: オブジェクアトリビュートなのかクラスアトリビュートなのかが激しくわかりにくい。

アトリビュートはどれもアクセッサを通して使うけど、そいつらのうちどれがクラスアトリビュートなのか、どれがインスタンスアトリビュートなのか見ても全然わからない。

結果、グローバルな副作用を常に意識しながらソースコードを改修しなくてはならない。

3:ベタ書きを推奨する→アトリビュート初期化のタイミングが制御しにくい

クラスアトリビュートを使ってるとその指定を.pmファイル内にベタ書きする事が多くなる。Catalystとかでもそうだよね。ある一定の効果はあるので、完全否定じゃないんだけど、アトリビュートとかを全部これで初期化しはじめると・・・微妙。

普通オブジェクトはインスタンス化するタイミングでアトリビュートが初期化されるべきなのに、クラスが*読み込まれた*時点で初期化されちゃう。結果、設定等をBEGIN { } で書く必要があったりする。どうなんだ、これは。

4: アトリビュートのライフサイクルが指定できない→同クラスのオブジェクトインスタンス変数とライフサイクルが違うので困る

クラスアトリビュートはPerlの場合基本的には永続。明示的に ->attr( undef ); や ->attr( $new_value ); とかしないと値がリセットされることはない。どのタイミングでそれをしたらいいのか(していいのか)もわからない。

そのクラスが基本的にデータを貯蔵してるだけのクラスならいいんだけれども、そこでさらに同じクラスでオブジェクトインスタンスを作ろうものなら今度はどの変数が永続して生きてて、どの変数がインスタンスが解放されると解放されて・・・とか考え始めないといけない。

オブジェクト指向で書くなら、オブジェクト本体のライフサイクルに合わせるべきで、これを考え出すと途端に辻褄を合わせるためだけのコードが発生すると思う。

否定ばっかりしていてもしょうがないので、代替策も。

  1. まず基本方針としてクラスデータを持ちたい場合でも極力インスタンス変数で我慢する。
  2. 初期化はそのオブジェクトを初期化するクラスから値を渡す。魔法で勝手に初期化しようとするとすぐグローバル変数に頼りたくなる。
  3. アプリケーション全体を一つのオブジェクトでラップする
  4. クラスデータは例えば*デフォルト値*を指定するのに使う

具体策も書こうと思ったけど、時間が切れたのでとりあえず以下次号!


    このエントリーをはてなブックマークに追加 mixiチェック

Spatial インデックス貼れるKVMってことで、おー!と思ったんだが、ぶっちゃけどうしたらいいものか迷う結果に。

自分の作った非同期アプリの中で、AnyEvent::DBI風味(ブロッキングする可能性のあるDBアクセスを別プロセスにわけて順番にクエリを処理しつつ、プロセス間でデータのやりとりを行う)なアクセス方法で試したみたところ、主キーによるデータ取得はMySQLベースのものより30%ほど高速だったものの( 700+ qps -> 900+qps)、Spatialインデックスによる検索が30%ほど逆に落ちてしまった(540 qps -> 380 qps )。MongoDBのspatialインデックス貧弱だなぁと脊髄反射で思ったんだけど、よく考えるとMongoDBのPerlバインディングかもしれん。APIはオブジェクトっぽく洗練されてるんだけどムダが多すぎ。もっと速くできるだろ!この辺りはもう少し調査が必要。

あと、データが無くなる可能性が高い事も様々な文献でみてわかった。どのデータベースも結局のところハードディスクに確実に書き込まれるかなんて時と場合によってわからないんだけど、MongoDBの場合は「そこはちゃんと対応しようとするとパフォーマンスヒットがあるからとりあえずレプリケーションとスナップショットを取るようにして回避してよ」というポリシーがありきで、ディスクに書き込めたかどうかをなるたけ追求する、という事ではないらしい。まぁそれはポリシーの問題だから良しとしよう。しかしそれなら自分は「使い方としてはMemcachedと同じくらいに考えておけ」という事だと受け取った。つまり揮発性のあるデータストレージだということ。

そうなると話は全然変わってくる。MongoDBをプライマリストレージにするわけにはいかない。MySQL等でマスターデータを持っておいて、それを定期的にMongoDBにデプロイする形になるんだろうな。

レプリケーション設定は比較的簡単だと認識。

インストールも超絶簡単。起動も超絶簡単。

ふーむ。色々微妙だな。


    このエントリーをはてなブックマークに追加 mixiチェック

まだこう、細かいpros/consがわからないのでなんとも言えないんだけど、とりあえずaio_open/aio_writeと普通のopen/print/closeで同じ事した場合とでベンチマークとか取ってみた。これでいいのかなー

環境はMac OS X 10.5.8, 2.4 GHz Intel Core 2 Duo, 4GM RAM.

Comparing with buffer size 10...
         Rate normal    aio
normal 80.0/s     --   -19%
aio    99.0/s    24%     --
Comparing with buffer size 100...
         Rate normal    aio
normal 80.0/s     --   -18%
aio    97.1/s    21%     --
Comparing with buffer size 1000...
         Rate normal    aio
normal 76.9/s     --   -13%
aio    88.5/s    15%     --
Comparing with buffer size 10000...
         Rate normal    aio
normal 52.4/s     --   -27%
aio    71.9/s    37%     --
Comparing with buffer size 100000...
         Rate normal    aio
normal 15.9/s     --   -63%
aio    42.7/s   169%     --

コードはこちら、githubで。
    このエントリーをはてなブックマークに追加 mixiチェック

前のエントリ書いたら宮川さんにオススメ方法を教えてもらった

http://twitter.com/miyagawa/status/10271128928
plackup \
      -L Shotgun
      -MPlack::App::WrapCGI
      -e 'Plack::App::WrapCGI->new(script => "/path/to/cgiscript.cgi")'

だそうです!これなら全ての要件を満たせるもよう!そしてapp.psgiを作る必要ありません。

今これやってみて一つだけ問題点。多分POD入りのCGIは動かない。それと、__DATA__は見てくれるけど、__END__があると動かない。__END__さっき簡単なパッチのpull requestを送っておきました!

なお、-L Shotgunを使うと CGIファイルの中身は実行時までコンパイルされないのでご注意(つまり、plackup -rもいらないってことですね)
    このエントリーをはてなブックマークに追加 mixiチェック

dve_deprecated.png

I'll support it for a while should there be any problems , but in general, please consider using Data::Recursive::Encode instead.

Data::Visitor::Encodeの開発を停止します。もうしばらくの間は問題等あればサポートしますが、基本的にはData::Recursive::Encodeを使って下さい。
    このエントリーをはてなブックマークに追加 mixiチェック

ミニブログサービスで先行するTwitterと差別化を図った上で新たなサービスを出すのは面倒くさいので、id:miyagawa氏のコードを丸々パクった上でTwitter支援ツール、Hamakiを昨日の夜からちょこちょこと開発しました(本当はTwitter以上に色々できるんですが、そのあたりはまださわってない)

もう自分がやりたい事はだいたいできるようになったのでとりあえず公開です。

まず前提としてTwitterを使ってていくつか不満な点があったわけです:

  • Twitterで特定のアプリの発言がちょっぴり気になるので自分のTLに表示したくない
  • かといって上記アプリの発言している人たちの他の発言は読みたいので、それらの人をremoveするのもしたくない。
  • あと、全般的にtwitterをリロードするような事をしていると非常に効率が悪い
  • かといって、今のところアプリを気に入ったアプリも存在しないし、第一おれは色々カスタマイズしたいんじゃ、ゴルァ。
ってことで、Plackベースの非同期エンジンTatsumakiを使用したHamakiです。こんな感じで見れます。

hamaki.png

ほしかった機能としては、まず自動ロード。一回ページを読み込んでしまえば、その後は置いておけばJSが勝手に新規発言をロードしてページをリフレッシュしてくれます。

これはAnyEvent::Twitter::StreamでTwitter Stream APIを使うことによって実現してます。ちょっと仕組み上、普通のTwitter TLとは表示内容が違ってきますが、僕はこっちのほうが好きだったりします。

あと、フィルタリングができます。例えば、発言内容によって更新を無視したりできるわけです。

ちなみに今回使った設定はこれですね。ごめんね、radiotube。
で、最後に、元々のパクったソースコードにはなかった機能、Twitterに自分の新発言を送信する部分を追加したので、このUIだけでTwitter的な事はほとんどできてしまうことになります。まぁ、DMとか@自分とかはみれないけど、そんなんたまにしかみないし・・・

設定ファイルを書いたらあとは./script/hamaki.pl --configfile=config.yamlってするだけ!サーバーが立ち上がるのでそこにブラウザで /chat/twitterとかにアクセスすれば上記画面がでて勝手に更新してくれますお。

とまぁ色々書きましたが、実際の構造のほとんどはid:miyagawa氏のものですね。元々Tatsumakiに入っているデモ用のスクリプトdemo.plが素晴らしかったので、僕はこれをががっとファイルごとにわけて整理して、スコープが違ってもデータを共有できるようにして、Twitterにポストする機能と、それを実現化するためのTatsumakiフレームワークの拡張を子クラスで行った程度です。なので、miyagawa++, Tatsumaki++, Plack++ なのでした。
    このエントリーをはてなブックマークに追加 mixiチェック

神南でお茶中。「daisuke」アカウントでlocal::libを設定してあったのをすっかり忘れて"sudo cpan"してたら、/usr/local/lib/perl5に最新のモジュールが入っているのに、~/perl5 から重複するモジュールが先に検出されておかしな事になる、というエラーにあってしまった。

みんな、local::libしたらsudoとかしないで、そのままcpanだよ!


    このエントリーをはてなブックマークに追加 mixiチェック

前のエントリでData::LocalizeをMouse化したと書いたが、それぞれこんな感じ:

daisuke@beefcake $ env ANY_MOOSE=Moose perl -Mblib tools/benchmark/benchmark.pl Rate locale_maketext data_localize locale_maketext 9615/s -- -64% data_localize 26786/s 179% -- daisuke@beefcake $ env ANY_MOOSE=Mouse perl -Mblib tools/benchmark/benchmark.pl Rate locale_maketext data_localize locale_maketext 9615/s -- -66% data_localize 28037/s 192% --
    このエントリーをはてなブックマークに追加 mixiチェック

Data::LocalizeMooseベースで、Moose嫌いなtokuhiromがMouseじゃねーから使わないって言われたのが発端。おお、んじゃあAny::Mooseにすべかぁ、と思ったらParameterized typesが実装されていない。ないからことごとくエラー。「実装されてないじゃん!」って言ったらtokuhiromとYappoに「え〜、俺ら必要ないし」的な発言をされて正直Mouse Mouse言うならちゃんと最後まで面倒みてやれよ!と思った。

...というような事を書いたが、このエントリは別に彼らに文句を言いたいわけではない。

オープンソースの世界は参加者一人一人が自分の技術を少しずつ世間に提供する世界だ。彼らにMooseとの完全なる互換性を求めるのは大間違いだ。ここまでちゃんと使える物にしてくれたのはそもそも彼らなんだから、それはそれで充分とするべきだし、彼らがその開発のための犠牲になるのはおかしい。

そもそも俺がMouseのコード書けるかもしれない。面倒くさがってるのはこの場合おれのほうだよね。

というわけで、まず俺のできること第一弾はAny::Moose化はしつつ、Parameterized typesを使わない方法で実装してしまうこと。んで、しました。その後は家を掃除したり、ポトフを作ってみたり。正直この時点でまだMouseのコードを1行も見てない。

それが終わって、PCの前に戻ってきたら、やっぱり互換性がないと新規参入者達にMouseもMooseもすすめられねぇや、JPA的にまずい、と思って改めてMouseのコードを確認。・・・30分後、なんとなくわかってきた。あと、Parameterized typesってのは基本的に再帰的にデータの内容を確認する事だから、再帰呼び出しをする関数を書けばいいのもなんとなくわかってきた。Mouseを見ると基本的にisaという宣言がある部分は基本的にはクロージャを作っている:

# ちょっぴり実際のコードとは違います my $code = $optimized_constraints->{ $type } || sub { Scalar::Util::blessed($_) && $_->isa($type) };
これをベタ書きしてた所を、ジェネレータにすればいいのだ。そして毎回isaが呼ばれると作ってたsub{}も、キャッシュしてしまえ。というようなパッチを書き、Parameterized typesが動くようになった。コミット。Data::Localizeも無事Paarameterized typesを使えるようになった。

というわけで、MouseでParameterized typesが使えるようになったあらまし。問題がなければ次の0.19に入ると思う。
    このエントリーをはてなブックマークに追加 mixiチェック

結局ユーザー管理だとか、お金を振り込む事だとか、なんか色々やらざるを得ないと言うことから、自分で書いたほうが速いという決断でJPA用のサイトを一からCatalystで書いてます。

今回はこれをgithubで晒しているわけですが、今ふと思ったのは、普段から我々PerlプログラマはCPANでいろんなCatalystのプラグインだとかを見ていたり、もしくはあちこちで細かいティップスみたいなものを聞きかじってるわけだけど、ひょっとすると俺たちほとんど他人のCatalystアプリケーションって見たことない?

よくよく考えたら自分でさえも2,3回しかなかった。もちろん、社内のものは除く。完全に外部の奴ね。MojoMojoみたいな完全にできあがってるアプリケーションってなかなか外に出てこないし、そもそも一回作ってそのままインスコしておしまい、みたいなの多いもんな。

まぁそんな事を踏まえると、githubにソースコードを一通り晒しておくというのは、JPA的にもいいたたき台になってよいんじゃないかとか思ってる。やりかたの賛否両論はあるにしろ、ね。

とりあえずこのままがりがり書き続けて、近いうちにhttp://japan.perlassociation.orgにアップするのでもしご意見等々ありましたら是非お知らせください。
    このエントリーをはてなブックマークに追加 mixiチェック

おお、指摘ありがとうございます

DATA_LOCALIZE_DEBUG=1 を使ってデバッグしていただく、というそもそもの目論見の部分がうまく活用できたようでよかったよかった。

ってことでさっき0.00002をアップしました。
    このエントリーをはてなブックマークに追加 mixiチェック

Data::Localize 0.00001をリリースした。やってることはLocale::Maketextと一緒。主な違い:

  • オブジェクトベース。
  • Locale::Maketextは名前空間をいじってどうのこうのするんですが、これはしない。基本的には登録されているハンドラを全て試す、っていうだけ。
  • デバッグ用のメッセージを一杯仕込んだので、なぜか国際化がされない場合は多分Locale::Maketextより問題箇所の判断が大分楽
  • Locale::Maketextより速い
速さに関しては、以下の通り:
daisuke@beefcake-7 Data-Localize$ perl -Mblib tools/benchmark/benchmark.pl Rate locale_maketext data_localize locale_maketext 9615/s -- -64% data_localize 26786/s 179% --

で、これをCatalystから使うためのCatalyst::Model::Data::Localizeを作ってみた。こいつはCatalyst::Plugin::I18Nを使わずに国際化するためのツール。といっても、Catalyst::Model::Data::Localizeは単純にCatalystの設定からData::Localizeを設定するためのレイヤーで、同梱のCatalyst::Plugin::Data::LocalizeというモジュールでCatalyst::Plugin::I18Nのようにlocalize()関数を生やすためのものです。Catalyst::Plugin::I18Nを使っていた方ならほぼそのまま使えるかと。

というわけで数週間前にLocale::Maketextの中味を解説しだした私は、結局国際化モジュールを自分で作ってしまった、という落ちでした。
    このエントリーをはてなブックマークに追加 mixiチェック

このパッチで多分直ると思う。管理者達が今お休みタイムなのでフィードバックがもらえてないけど、俺の環境ではOK
    このエントリーをはてなブックマークに追加 mixiチェック

わすれてた。昨日DateTimeX::Liteをアップしてた。
use DateTimeX::Lite; my $dt = DateTimeX::Lite->new(year => 2008, month => 12, day => 1); $dt->year; $dt->month; $dt->day; $dt->hour; $dt->minuute; $dt->second; # Arithmetic doesn't come with DateTimeX::Lite by default use DateTimeX::Lite qw(Arithmetic); $dt->add( DateTime::Liate::Duration->new(days => 5) ); # Strftime doesn't come with DateTimeX::Lite by default use DateTimeX::Lite qw(Strftime); $dt->strftime('%Y %m %d');
だいたいDateTimeと互換性のあるAPIで、ただPurePerlであるところと、とにかくlazyに色々ロードするようにしてある。TimeZoneとLocaleは全部一緒に入ってて、なおかつdo 'timezone.dat'みたいな感じで呼び出してるから大分高速。
あとはTimeZoneとかは一緒にバンドルしててもどうせ全部なんて使わないから、必要な分だけをあとからインストールできるようにしたいな、と。
    このエントリーをはてなブックマークに追加 mixiチェック

ちょっと前に書いたData::Localizeをさらに開発すすめて、dev releaseをCPANにあげてみた。
で、今のベンチマークはこんな感じ:
daisuke@beefcake-7 Data-Localize$ perl -Mblib tools/benchmark/benchmark.pl Rate locale_maketext data_localize locale_maketext 9585/s -- -65% data_localize 27273/s 185% --
という感じ。まだなんか機能的に抜けてるところあるかなー
    このエントリーをはてなブックマークに追加 mixiチェック

decoded_contentで文字コード明示するも化けらった

デコードするだけなら、この以下のコードをdecode前に足すだけで直ると思う

my @ct = grep { /^text/i } $res->header('content-type'); $res->header('content-type', @ct); print $res->decoded_content;

でもこれだけじゃあ答えが分かった、ってだけで後からこれを読んだ人のためにもならないので、以下どういう手順を取ったか説明してみる。

まず、最小限テストケースを用意。

実行するとばけらった。問題を確認した。この問題に関しては、decoded_content()が色々と問題があるのを事前に知っていたのでperl -dでデバッガモードで検証開始。

DB<1> HTTP::Message::decoded_content(/usr/local/lib/perl5/site_perl/5.8.8/HTTP/Message.pm:201): 201: if (my @ct = HTTP::Headers::Util::split_header_words( \ $self->header("Content-Type"))) {

あーなんかこのあたりでヘッダみてて怪しいなぁ・・・と思って、x @ctで中味を確認

DB<1> x @ct 0 ARRAY(0x8d334c) 0 'text/html' 1 undef 2 'charset' 3 'EUC-JP' 1 ARRAY(0x9f0be8) 0 'application/xhtml+xml' 1 undef 2 'charset' 3 'EUC-JP'

む。確かdecoded_content()はヘッダ内容とかでデコードの仕方を変えてるはず。これ、怪しいなぁ。その後、"n"でステップを薦めていくと以下の行に:

DB<2> n HTTP::Message::decoded_content(/usr/local/lib/perl5/site_perl/5.8.8/HTTP/Message.pm:293): 293: if ($ct && $ct =~ m,^text/,,) {

で、本来ならここに入っていかないとおかしいよね。テキストのデコードなんだから。でもそこに行かないで終わってしまった。ってことはapplication/xhtml+xmlがいけないんじゃね?ってことで以下のスクリプト。

use LWP::UserAgent; my $ua = LWP::UserAgent->new(); my $res = $ua->get('http://yugioh-wiki.net/index.php?%A5%C8%A5%C3%A5%D7%A5%DA%A1%BC%A5%B8'); my @ct = grep { /^text/i } $res->header('content-type'); $res->header('content-type', @ct); print $res->decoded_content;

これで見事動いた。これはバグかどうか微妙なところだよな。applicationで始まるMIME-Typeをデコードしろって言われても・・・ねぇ。xmlだったらテキスト、って判断すればよいのか?
    このエントリーをはてなブックマークに追加 mixiチェック

MENTAやNanoAでもextlibにつっこむ云々の話はあったが、とりあえずインストール先のサーバーにアクセス権があるという前提のもと、アプリケーションの依存関係を全部extlibに自動的につっこむスクリプトを書いてみた。

use strict; use CPAN; use Module::CoreList; use Module::ScanDeps; use File::Find::Rule; use File::Spec; use File::Temp qw(tempdir); if (scalar @ARGV != 2) { print <<EOM; Usage: deps2extlib.pl target [extlib] target - the directory or script that you want to compile dependencies against. extlib - the path where you want to install your dependecies. EOM exit 1; } my $target = $ARGV[0]; my $dir = File::Spec->rel2abs($ARGV[1] || 'extlib'); unshift @INC, $dir; my @files; if (-d $target) { @files = File::Find::Rule ->file() ->or( File::Find::Rule->name('*.pl'), File::Find::Rule->name('*.pm') ) ->in($target); } else { @files = ($target); } my $h = scan_deps_runtime( compile => 1, files => \@files, recurse => 1, ); my %seen; my $corelist = $Module::CoreList::version{ $] }; CPAN::HandleConfig->load unless $CPAN::Config_loaded++; my $home = tempdir(CLEANUP => 1); local $CPAN::Config->{cpan_home} = $home; local $CPAN::Config->{keep_source_where} = File::Spec->catdir($home, 'sources'); local $CPAN::Config->{build_dir} = File::Spec->catdir($home, 'build'); local $CPAN::Config->{prerequisites_policy} = 'follow'; local $CPAN::Config->{makepl_arg} = "INSTALL_BASE=$dir"; local $CPAN::Config->{mbuild_arg} = "--install_base=$dir"; local $CPAN::Config->{mbuild_install_arg} = "--install_base=$dir"; foreach my $key (sort keys %$h) { next if $key =~ /^unicore/; next if $key =~ /^auto/; $key =~ s/\//::/g; $key =~ s/\.pm$//; next if exists $corelist->{$key}; my $dist = CPAN::Shell->expandany($key)->cpan_file ; if ($dist =~ /\/perl-/) { warn "won't try to install perl, but we found dependency on $dist"; next; } next if $seen{ $dist }++; CPAN::Shell->install($dist); }
こんな感じで、アプリケーションのディレクトリかスクリプトファイルなどを第1引数に指定して、第2引数にはこれらの依存関係をつっこむ先を指定する:
perl deps2extlib.pl /path/to/myapp /path/to/extlib
同じmyapp ディレクトリにextlibがある場合は考えてなかったな。まぁ、それだけきをつけて!
    このエントリーをはてなブックマークに追加 mixiチェック

Locale::Maketextはそれはそれでいいとして、とりあえずData::Localizeってのを書いてみた。まだエンコーディング対応や.moファイル対応してないので完成とは言い難いけど。

とりあえずcodereposにあります
    このエントリーをはてなブックマークに追加 mixiチェック

さて、今度はLocale::MaketextとLocale::Maketext::Lexicon。

Locale::Maketextは基本操作は以下のようなイメージ。get_handle() というメソッドでローカリゼーション用のオブジェクトを引っ張り出し、それに対してmaketext() を呼び出して変換を行う。
use MyApp::I18N; my $handle = MyApp::I18N->get_handle('ja'); print $handle->maketext("Hello, World!"), "\n";

get_handle() になにげに言語IDが渡されているのもポイント。引数無しでget_handle() を呼び出すと、前出のI18N::LangTag::Detectのコードとかが自動的に呼ばれる。もしこの辺りを自分でコントロールする必要がないと感じるのであれば、引数無しでもよいかもしれない。

話は戻って、MyApp::I18Nを設定するのに、Locale::Maketextを継承した国際化用のモジュールを作る必要がある。
package MyApp::I18N; use strict; use base qw(Locale::Maketext); 1;

国際化ルール自体は、上記MyApp::I18N名前空間以下に置かれる。Locale::Maketextは基本的に.poファイルや.moファイルには対応しないので、直接ハッシュに指定する形になる。
package MyApp::I18N::ja; use strict; use base qw(MyApp::I18N); our %Lexicon = ( "Hello, World!" => "みなさん、こんにちは!" ); 1;

この名前空間が前もって固定されている、というところに「後付で(プラグイン等から)国際化ルールを追加できない」という制限が出てくる。もちろん、%Lexiconハッシュに直接ぶっこめばいいんだけれども・・・それ用のコード書くのは微妙じゃないか?というわけでなんとなく自分はこのアプローチはよくない気がする。

Locale::Maketext::LexiconはLocale::Maketextから継承とかなんもしないくせに、Locale::Maketextを拡張している。意図はいいんだけど、APIデザインとかに全くセンスを感じられないモジュール。こちらは上記Locale::Maketextを使うときと同じくMyApp::I18Nの設定はするのだけれども、この際に.poファイルや.moファイルの扱いができるようになる。要はLocale::Maketext::Lexiconはそれらのファイルの中味を解析して、%MyApp::I18N::xxx::Lexiconにぶちこんでくれるわけです。
package MyApp::I18N; use strict; use base qw(Locale::Maketext); use Locale::Maketext::Lexicon { '*' => [ 'Gettext' => '/path/to/localization/*.po' ], # ファイル名じゃなくてパス中にen/jaとかがはいるなら、以下のようにする: # '*' => [ 'Gettext' => '/path/to/localization/*/data.po' ], _decode => 1 }; 1;

上記の場合全ての言語(「*」という表記)の国際化データは、gettext形式(.poファイル)で/path/to/localization/ディレクトリ以下の、それぞれの言語IDの名前の付いたファイルに格納されている、と表記してある。Locale::Maketext::Lexiconの場合はあとは.poファイルを作るだけ。

ちなみにAPIは最悪なのですが、Locale::Maketext::Lexiconは後からPerl形式以外の国際化データを使えるというのと、それらを後から追加できるというのが最大のメリット。
eval "package MyApp::I18N; Locale::Maketext::Lexicon->import( { ja => [ Gettext => '/path/to/another/localization/ja.po' ] _decode => 1 }); "

とかすると、新しい日本語データを追加することができる。何回も言うけどAPIは最悪。パッケージ名を指定しなくちゃいけないとか、本当にアホだけど、しないとできない。

あとカバーしてないのは_AUTOとかかな。以下次号。
    このエントリーをはてなブックマークに追加 mixiチェック
カテゴリー
記事検索
メッセージ

名前
メール
本文
アーカイブ

このページのトップヘ