D-6 [相変わらず根無し]で“moose”タグの付いているブログ記事
Moose + POEなIRCボット作ろうと前に思い立って、結構長いこと弊社内で使ってたんですが、このたびAnyEvent化して色々モダナイズしました。やっぱり非同期なhttp_getしてる部分とかは抜群に速いなぁ。
AnyEvent したついでにAnyEvent::MPにも対応したですよ。POE::IKCと違って、一旦セットアップしちゃえば、Morrisの外から何かをポストするのにコマンドラインから一発でできる;
aemp morris privmsg "#channel" "Your comment"
ただMPってセットアップが結構面倒くさいのね。以下はまったり考え込んだりしてしまったところ:
- 最初動かない時になにやってんんだかわからないので、PERL_ANYEVENT_MP_TRACEとPERL_ANYEVENT_MP_WARNLEVELを設定したほうがいい。後者は10くらいにしないとなんも有用な事を言ってくれない。
- seedの役割をするプロセスが必要。Morris自身がseedになれないかなーと思ってやってみたけど、トレースで「refuses to talks to myself」みたいなこと言われた。
- .perl-anyevent-mpファイルに受け側のbind情報を書いておいたほうがデバッグは楽かも。実際に動かす段階ではどのポートでも使えるようにしたほうが簡単かもしれない。
- rcv/sndとportの使い方、シンタックスエラーにならない限り間違った使い方をしていてもなんの注意もしてくれないので、よくわかってない間はAnyEvent::MPディストリビューションの eg/内を見たほうがいいかもしれない。
- MPやAnyEvent::IRCのようなネットワーク接続型のAnyEventモジュールを使っていると環境によっては"Out of memory!"エラーが出てくる事がある。もしバックエンドを特に指定しないでAnyEventを動かしている場合は、素直にEV.pmをアップグレードしてみるとよい。なぜか直る。
以上。Morris君は結構お気に入りなのであった。
I'm just going to wear my Japan Perl Association Head Director's hat for a bit and add a small, but what I consider to be something very important about the whole Moose or no Moose argument:
"Moose Developers, please act like you care (more) about startup cost, use of RAM, and other efficiency issues."
Here's what I mean by the above:
I'm an engineer. I like Moose. I understand the whole Moore's law thing, I understand the "buy more RAM" thing.
However, most of the world is made up of non-engineers. This group is made up of people who make decisions, or they are your regular joe programmer who don't give a rat's ass if they use Moose or plain hash or worse, some other language.
What these people see are things like numbers. If they see a benchmark touting Moose to be slower, they'll ONLY remember that Moose was lower ranked. They won't remember the context in which the benchmark was taken, nor Moose's advantages.
Furthermore, if a Moose developer responds to that kind of benchmark with a smart-ass comment, or a phrases like "dude, buy more RAM", it just doesn't do anything to convert people's minds. I'd really like to avoid that.
I know Moose is great. I know something like it was long overdue. I'd like to keep using it on all of my projects -- which I try to do now.
But if a client sees these articles and get the wrong idea, I'm going to have to spend another 2 weeks writing up evidence claiming Moose's superiority before I can install it in their systems. And if that happens, I'm just going to whip my Perl-fu and say "bless this hash, and let there be no Moose" -- cause it costs money to prove otherwise.
So, here's my $0.02.
I know that the Moose developers are doing a great job. I'm not even going to say that they need to change their attitude or goals. However, I'd really like them to make a more thought-out response, and act like you actually care about the parties involved that have a slightly different view than yours (in this case, people who really care about speed, memory efficiency, etc). I actually don't care if they actually care. I know they are good enough that they won't completely ignore it. And I/we will contribute where possible. Just... act like you care :)
That will make my life easier. I can point people to the Moose roadmap or whatever and say, "look, they care. they /will/ address your concerns. let's just install Moose in your system for now, yeah?" and go on hacking with Moose.
And you know, you can always mention/encourage more activities like JPA was doing with gfx to shove some startup costs ;)
(update: fixed typo and such)
Algorithm::BIT - http://d.hatena.ne.jp/naoya/20090606/1244284915
以下、全然そうする必要はなかったけど、敢えてMoose化をしてみた。なんとなく、例としてご参照ください。
ppackage Algorithm::BIT;
use Moose;
use MooseX::AttributeHelpers;
use namespace::clean -except => qw(meta);
has size => (
is => 'ro',
isa => 'Int',
required => 1,
);
has data => (
metaclass => 'Collection::Array',
is => 'ro',
isa => 'ArrayRef',
lazy_build => 1,
provides => {
get => 'data_get',
set => 'data_set',
}
);
sub _build_data {
my $self = shift;
return [ map { 0 } 1 .. $self->size ]
}
sub BUILDARGS {
my ($self, $n) = @_;
return { size => $n };
}
sub read {
my ($self, $idx) = @_;
if ($idx > $self->size) {
confess 'assert: index overflow';
}
my $sum = 0;
while ($idx > 0) {
use integer;
$sum += $self->data_get($idx);
$idx = $idx & $idx - 1;
}
return $sum;
}
sub update {
my ($self, $idx, $val) = @_;
my $max = $self->size;
while ($idx <= $max) {
use integer;
$self->data_set($idx, $self->data_get($idx) + $val);
$idx += ($idx & -$idx);
}
}
__PACKAGE__->meta->make_immutable;
1;
(5/13 追記): メソッドモディファイヤーの件、Catalyst開発チームにテストパッチを送ろうと思って色々書いたら理由がわかったので、追記しました
それについての一般論は JPA #02で話すのでおいておきますが(参加申し込みは今日5/12までですよ!)、5.8 からMoose化したCatalystにポートを行う際に実際にあった問題・注意点をちょっと書き出してみます。
1. use Catalyst
Catalyst::Upgradingを読んでいると
package MyApp;
use Moose;
extends 'Catalyst';
__PACKAGE__->setup(qw/
ConfigLoader
/);
という表記が見られるが、これは気をつけないと駄目。
自分が直面した問題は、path_to()等を使った時に起こった。path_to() は現アプリのルートディレクトリからのパスを指定したい時に使う。例えばTTテンプレートのコンパイルされた物をMyApp/tt2 以下に格納したければ、
my $path = MyApp->path_to("tt2");
というようにする。このpath_to() というのは $c->config->{home}という値を使用するのだが、この値は通常Catalystによって自動的にMyApp/ 以下に設定されるので、デフォルト状態のファイルレイアウトでCatalystを使用する分には特に問題にならない。だが、上記のextends記法を使っているとなぜか home変数が設定されず、path_to()の挙動がおかしくなる。
これを回避するには、home変数を設定する必要があるのだが、homeの自動設定はCatalyst::import()で行われており、通常extends() (use baseでも一緒)を使うとimport()は実行はされないのだ(importが自動的に実行されるのは、「use Module」とした時だけ)。それはすなわちhomeの初期化が行われない、ということである。
ということで、実際はこうする必要がある:
package MyApp;
use Moose;
use Catalyst;
extends 'Catalyst';
2. :Index, :Private, :Local, :Chained等
CatalystでコントローラーメソッドをURIパスに結びつけるのが:Indexや:Chained等のメソッドアトリビュートと呼ばれる物だが、これらはPerlのコンパイルフェーズで処理される。いわゆるBEGIN {} ブロックが走るタイミングと考えて良い。
コンパイルフェーズというのは特殊で、use() 宣言やBEGIN{}ブロック等、Perlのネイティブレベルで定義されたものだけが(注:割愛するが、実はそれ以外の黒魔術的なやりかたもある)、それ以外のコードを走らせる前に全部動く。本当のコードが走る前の一種の初期化フェーズと考えてもいいかもしれない。
通常Mooseマニュアルには継承を行う際はextendsを使えと書いており、同じノリでMyApp::Controller::RootをCatalyst::Controllerから継承する、というようにMooseで書こうとすると以下のようになる:
package MyApp::Controller::Root;
use Moose;
extends 'Catalyst::Controller';
sub index :Index {
....
}
が、ここが落とし穴。これだと :Indexを見た瞬間にPerlがエラーを吐く。なぜか。
:IndexをPerlが最初に見るのはコンパイルフェーズだ。前述の通り、use宣言やBEGINブロック以外はその段階では実行されない。:Indexを理解するためにはMyApp::Controller::Rootはその時点ですでにCatalyst::Controllerを継承していなければならないのだが、継承を指定するextends宣言は普通の関数なので、コンパイルフェーズでは走らない。よって、コンパイルフェーズでは MyApp::Controller::Root->isa('Catalyst::Controller')にはなっておらず、メソッドアトリビュートも理解されないのだ。
これを回避するのには単純にBEGINブロックで囲ってやればいいのだが、正直一瞬意味がわからなくて困る
package MyApp::Controller::Root;
use Moose;
BEGIN { extends 'Catalyst::Controller' }
sub index :Index {
....
}
3. プラグインが実行されない
(5/13 追記) 以下の理由がわかった。単純。Catalyst->setupはMooseのメソッドモディファイヤーを使用する前に呼び出されなければならない。
package MyApp; .... before finalize => sub { ... }; __PACKAGE__->setup(...); # これは駄目
package MyApp; .... __PACKAGE__->setup(...); # これならOK before finalize => sub { ... };
Catalystは過去の経緯から、setup()時にクラスの継承関係を変更するのだが、それをやる前にメソッドモディファイヤーを使ってしまうと親クラスをきちんと認識してくれない、ということっぽい。setup()の中で親クラスのリセットを行ってくれればそれでいいんじゃねと思うが、それまでは単純にMyApp.pmの上のほうでCatalyst関係の初期化を行えば良い、ってことですな。
プラグインの多くは継承のメカニズムを使ってCatalyst内のメソッドの前か後に実行されるように書かれている。
Catalyst 5.8からはMooseなので、プラグインを書くほどでもないちょっとした事ならMyApp内でメソッドモディファイヤー(afterやbefore)を使ってちょこちょこと自前のロジックを足したりもしたいのだが、それをすると突然プラグインが動かなくなったりする。
自分がはまったのはCatalyst::Plugin::Unicode。それまで動いていたのに、以下のようにbeforeメソッドモディファイヤーで自前のエラー処理をつけようとしたら、"Wide character in syswrite"というエラーが出始めた。
package MyApp;
...
before finalize => sub {
my $c = shift;
$c->handle_exception if @{ $c->error };
};
色々と試してみたところ、これはメソッドモディファイヤーが適用されるタイミングに左右されることがわかった。モディファイヤーがCatalystの継承によるディスパッチを邪魔して、Catalyst.pm以外のメソッドを呼び出してくれない(ちなみにこれでより一層Moose::Roleが素晴らしい、ということがわかった。継承を意識しながらメソッドディスパッチとかうざすぎる)
ちょっとまだ一般解まではわかってないのだが、とにかく、上記のようにMyApp内からfinalizeにフックしたいなら、overrideとnext::methodを組み合わせるのが吉
package MyApp;
...
override finalize => sub {
my $c = shift;
$c->handle_exception if @{ $c->error };
$c->next::method(@_);
};
このようにすると正しくプラグインへのディスパッチを行いつつ、フックもできる。
4. エラーがよくUnknown Errorになる。
Perl 5.10.x ではUnknown Errorがよく出る、というのは聞いていたが、5.8.9でもなぜかなる。あんまりちゃんと調べてないので、そもそもこれは既知の事だったらあれなのだが、わかっているのはこれはとにかくコンパイルフェーズやその他セットアップしている時に起こるということ。
なので、このエラーが出たら、まずモジュール類がちゃんとコンパイルすることから調べ始めるのを薦める。それとちょうど良い機会なので、エラーを吐きそうなブロックは明示的に囲ったりして自前でエラーを出力するようにすればよい。warn && confessみたいな単純な処理でも以外と役立つ。
eval {
Class::MOP::load_class($pkg); # 個人的にはこういうのとか、DBICのconnect()とかでよくなった
};
if ($@) {
warn && confess;
}
いずれにしろ、エラーケースはちゃんと自前で処理したほうが良いので、これを機に怪しいところは全部やっつけるといいのではないだろうか。
とりあえず、今回Catalyst 5.7 -> 5.8に移行した際に直面した問題はだいたい以上のようなもの。それまでのCatalystでは出ない類のエラーが多いので、困る人も多かろう。なんらかの手助けになれば幸い。
Data::LocalizeがMooseベースで、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に入ると思う。

