As Sloth As Possible

可能な限りナマケモノでありたい

プログラミング

iOS Advent Calendar 2011 5日目 / NSURLProtocolの使い方

どうも、「iOS Advent Calendar 2011」5日目担当のfaultierです。つい最近使ったのでNSURLProtocolネタで。

NSURLProtocolって何?

Foundationフレームワークで最初から扱えるプロトコルはhttp、https、ftp、fileの4つ。これ以外のプロトコルでの通信をNSURLConnectionやNSURLDownloadなどで扱う場合や、特定のリクエストに限って特別な処理をしたい場合などに、NSURLProtocolを継承して登録することで使えるようになる。ちなみに、他のアプリからopenURLしたときにアプリを起動させるカスタムURLスキームとはまた別なので注意。こちらはアプリ内でURL Loading Systemを使うときにだけ影響するもの。

使い方

最低限必要なのは、+canInitWithRequest:、+canonicalRequestForRequest:、-startLoading、-stopLoadingの4つ。

まずはこんな感じで、どんなリクエストのときにそのURLProtocolをが処理するのかを決める。

+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
    return [[[request URL] scheme] isEqualToString:@"udon"];
}

ここでYESを返すとこのクラスがインスタンス化されて通信処理に進む。NOの場合は他に登録されているURLProtocolのこのメソッドが登録時の逆順に呼ばれて行く。この場合はudonというスキーム、例えばudon://marukame/bukkake/coolみたいなURLへのリクエストの時に処理をすることになる。別にこれは独自のスキームである必要ではなく、httpやfileなどをフックすることもできるし、特定のホストや特殊なヘッダが付いている時だけ処理するようなこともできる。

次は+canonicalRequestForRequest:。リクエストをcanonicalな形に変える必要がある場合はここで弄るのだけど、特に何もすることが無ければrequestをそのまま返してやればいい。

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
    return request;
}

その後実際の通信が始まり、-startLoadingが呼ばれる。NSURLProtocolのオブジェクトは、id<NSURLProtocolClient>のclientと、NSURLRequestのrequestというプロパティを持っているので、requestからどんなリソースが必要なのかを判断して、clientに対してデータを返してやる、という形で通信の中継をする。NSURLProtocolClientのメソッドは大体NSURLConnectionのdelegateと対応しているので、NSURLConnectionでの通信を実装したことがあれば分かるはず。NSURLConnection側でキャンセルされたときはstopLoadingが呼ばれる。簡単な例としては以下のようになる。

- (void)startLoading
{
    // 本来非同期で通信するのだけど、
    // この例では単に文字列データを返すだけなので、
    // その場で返してしまう
    NSData *data = [@"うどんが食べたい" dataUsingEncoding:NSUTF8StringEncoding];
    NSDictionary *headers = [NSDictionary dictionaryWithObjectsAndKeys:
                              @"text/plain", @"Content-Type",
                              [NSString stringWithFormat:@"%d", [data length]], @"Content-Length",
                              nil];
    NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:[self.request URL]
                                                              statusCode:200
                                                             HTTPVersion:@"1.1"
                                                            headerFields:headers];
    // NSURLResponseのオブジェクトを返し、
    [self.client URLProtocol:self
          didReceiveResponse:response
          cacheStoragePolicy:NSURLCacheStorageAllowedInMemoryOnly];
    // データを渡し、
    [self.client URLProtocol:self didLoadData:data];
    // 通信を終了。
    // 通信失敗の場合は -URLProtocol:didFailWithError:を呼ぶ。
    [self.client URLProtocolDidFinishLoading:self];
}

- (void)stopLoading
{
    // この例ではキャンセルしようが無い…
    // 内部で別なNSURLConnectionを使っていたり、
    // NSOperationQueueやGCDなどで非同期処理をしている場合、
    // ここでキャンセルの処理を実行する。
}

これだけだとudonスキームはまだURL Loading Systemに登録されていないので、適宜NSURLProtocolの+registerClass:、+unregisterClass:呼んであげることで使えるようになる。その独自スキームを使うUIViewControllerとか、あるいはもうクラスのloadメソッドで登録してしまうとか。

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [NSURLProtocol registerClass:[self class]];
    });
}

使いどころ

それどういうときに使うの?http以外のプロトコルとかそんな実装しないよ?と思うかもしれないけど、意外と使い道がある。UIWebView等での通信は裏側でNSURLConnectionを使っているので、WebView内での通信をフックしてアプリ側で弄ったりできるのだ。例えばCoreDataにあるデータをJSON形式にして返すようにしておくとか、JSからのトリガーでアプリ内のバックグラウンド処理を走らせるとか、画像をファイルキャッシュしておいてオフラインでも表示するとか、複数の異なる形式のWebAPIをプロキシして同じ形で扱えるようにしたり、ということも簡単にできる。

また、逆に「特定のリクエストに限って特別な処理をする」ではなく「特定のリクエストはスルーするけど他はブロックする」のような使い方もできる。最近(というには結構前からだけど)話題になったのは、iPhoneのWebViewにこういう脆弱性がある、という話。

何故デバイスのアドレス帳や着信履歴みたいなのの生のデータにJSからアクセスできる必要があるのか良く分からないし、おかしな仕様だと思うのでバージョンアップで塞がれるだろうとは思うけど、少なくともiOS5.0まではこの問題は残っているので、「アプリ内でUIWebViewにloadHTMLStringさせて、外部から取得したHTMLをfile://等のスキームで表示させている」ような場合には対処する必要がある。簡単な対策としてはきちんとbaseURLを設定しておけばいいのだけど、そうすると今度はCSS、JS、画像などのバンドル内部に持っているリソースが使えなくなる。「外部から取得したHTMLをアプリ内のUIWebViewで表示したいけど、アプリ内の安全なリソースにはアクセスさせたい」場合に、NSURLProtocolで通信をフックする仕組みが使える。

前者の記事だと、NSURLProtocolのサブクラスでバンドル内のファイルを開いて、そのデータを返してしまう、というやり方をしている。また、後者の記事やPhoneGapの実装では、NSURLProtocolの「登録時の逆順に対応できるかどうかチェックする」「YESを返せばそこで止まり、NOを返すと次のNSURLProtocolのチェックに進む」という性質を利用して、「ホワイトリストに載ってないリクエストの場合は、canInitWithRequest:でYESを返した上でエラーなり空レスポンスなりを返し、ホワイトリストに載っている場合はNOを返して通常のhttpやfileプロトコルとして処理させる」という方法でサンドボックス外のリソースにアクセスさせないようにしている。

まとめ

  • NSURLProtocolを使えばさくっと独自スキームを定義できるよ
  • WebViewみたいに細かく制御しづらいものの内部の通信もフックできるよ
  • セキュリティ面でも意味があるよ

という感じなので、是非試してみてください。

faultierfaulist  at 16:39  | コメント( 0 )  | トラックバック( 0 )  | この記事をクリップ!

git flowを使ってみることにした

ふと思い立ってA successful Git branching modelとかを今頃読んでみたり。

最近のお仕事、基本的に一人でiPhone/Androidアプリを作ってて同じコードを複数人で弄ることがなくて、しかも大体の案件が一月くらいかけて一から作るとか丸ごと作り直すとかそういう感じなので、割と開発プロセスとか考えずにフリーダムにやっちゃってるので、まぁあまりよろしくないなと反省をしている。かといってgitの機能を隅から隅まで使いこなしてるとか自分にとって最高にやりやすい状況になってるわけでもなく、むしろあんまり開発サイクルが上手く回ってなくて、でも人にすぐ渡せる状態かというとそうでもなく、というわけで意識的になんかの約束事に乗ってみることにした。

で。ついでにgitconfigいじってたりああそう言えばと思い出してzshrcいじったりしてたら日が暮れてた。開発環境整備すんのなんであんな楽しいんだろな。やりすぎ良くない。

faultierfaulist  at 00:57  | コメント( 0 )  | トラックバック( 0 )  | この記事をクリップ!

ちょっと見てて面白かったので

なんか金縛りにあって眠れなくなったのでTwitter見てたら面白いやりとりをみかけた。すげー大雑把に要約すると、こんな感じ。

  1. Railsの勉強をしてる人が「なんでHello worldしたいだけなのにDB必要なの!?DB使うにしたってMySQLなんか今必要じゃないしチュートリアルではSQLiteとかにしといてくれたっていいのに!プリプリ!」みたいな呟きをする
  2. 「いやRails使うんなら普通DB要るでしょ。なんでそこに文句言ってるの。」とツッコミが入る
  3. 「とりあえずチュートリアルは一番簡単なやり方書いといてくれた方が親切じゃないですか。」と反論
  4. 「いやだから、Railsを使うのに一番当たり前の構成を作るのにはどうしたらいいかがチュートリアルでしょ。それが必要ないんなら別なの、例えばSinatraとか使えばいいじゃん。フレームワークの特性も分からずに使っておいてチュートリアルがおかしいとか筋違いだろ。」と再度ツッコミ
  5. 「Railsがどんな特性のフレームワークとか知らないから勉強してるのに…最初っからそういうの全部考えてやれって言われても…僕はただRailsを手っ取り早く学ぶ手段があればいいのにって思っただけなのに…」みたいに凹んでしまう

ってな感じ。んで、それと隣接して外野で「最初は知らないことだらけなんだからおまじないだって言っておけばいいじゃん、フレームワークの特性がーとか大鑑巨砲主義がーとかどうでもいいだろ、やってみなきゃ分かんないことだってあんじゃん」「いや不要なとこに不要なもの使おうとしてるから適切なのはこっちだって教えてやっただけだろ、なんでわざわざ面倒なことやろうとしてるのにフレームワークに文句付けてんだよ」みたいな論争が若干生じてたりとか。

なんちゅーかね。この件に関して言えば、「それRailsに文句付けるとこじゃねーよ」って指摘は妥当だとは思う。Railsは「中規模のアプリケーションを、一般的な構成で」作るときに楽になるように設計されてるものなので、それを知らないまま進むと大概後で苦労する(レールに乗るから楽なんであって、レールから外れると荒野だ。今は大分良いけど、昔は黒魔術に手を染めないとレールから外れることすらできなかったんだよ!)。

そんなの知らないから勉強してるんだろ、それは後から考えさせろって?いや、WAFの選択って、「どんな目的に何を使うか」も重要なことなので、何かWAFを学ぶってときにはそういう選択の力も付ける必要はあると思うよ。WAF一般について知りたいんじゃないんだ、Railsについて勉強してるんだって?だったらとりあえずRailsの流儀に沿っておきなよ、何か意味があるからそういう構成になってるんだよ。分からないなら文句付けてないで、何でそうなってるか考えようよ。

少しまとめる。

  • 何かにとりかかろうとして、チュートリアルの時点で「なんでこんなことせにゃならんのだ?」って思うなら、「自分がやろうとしてることに適切じゃないものを使おうとしている」か、「今取り組んでいることを正しく理解できてない」可能性を先に疑った方がいい。大概の場合は「あー俺が間違えてたわー」ってなるか「あーこういうことだったのかー」って後で分かる。まぁ本当に提供側が不親切だったりいい加減だったりすることもままあるけど、それにつっこめるのはある程度使い熟せてからだ。
  • 教える側は「これはおまじないだから、そういうことだって思ってやりなさい」を安易に使うべきじゃない。何にか躓いてるときは、何が間違ってて何が理解できてないのか把握する・させるチャンスなので、そのタイミングで教えてあげればいいのにと思う。あんまり深入りさせてなかなか最初のステップに到達しないのはアレだけど、「おまじないだからとりあえず無視して」って言っちゃうとほんの少し潜ってみる足掛かりすら失なう。
  • でも、分からないことを責めるな、とは思う。「うぇぶかいはつしゃもすなるれいるずをしてみんとてするなり」みたいな人に「お前はゴキブリ退治に第七艦隊を呼ぶのか?」みたいなツッコミ入れたってそもそも分かってなかったからそんなことしてるんでしょと。単に教えてあげればいいじゃない。

どうでもいいけどRailsは「最初の一歩」には丸っきり向いてないと思うんだけど、なんでみんなRailsやりたがるし教えたがるんだろね。不思議。

faultierfaulist  at 03:24  | コメント( 0 )  | トラックバック( 0 )  | この記事をクリップ!

#isucon で学ぶWebアプリの高速化の話

あるいは、お遊びチーム2号は一体何をしていたのかについて

ISUCONという大変白熱した楽しいお祭を開催するにあたって、その前夜祭的な環境試験のためのチューニング祭が社内の有志数名で行われていて、そのときに色々学んだことをおまけとして書いておきます。

ISUCONて何?

下記参照。

要するに、「閲覧者視点での振る舞いさえ満たしてくれれば何をしようと構わんからWebアプリのレスポンスを改善しなさい」というお題で、誰が一番速くできるか競うイベント。

最初にある程度環境が整備されてるサーバ4台と、主催者側が用意した参考実装のアプリとテスト用データが渡される。このアプリってのはごくシンプルな個人ブログだと思ってもらえば良くて、最新記事10件が表示されるインデックスページと、記事全文が見られる詳細ページがあり、記事の投稿とコメントの投稿ができて即時反映される。全てのページには「最近コメントが付いた記事10件」が表示されるサイドバーがあり、ヘッダ画像、スタイルシート、JavaScriptが読み込まれる。テストデータにはそれなりに大量の記事データとコメントデータが入っている。フロントのWebサーバはApache、DBはMySQL、アプリはPerlとRubyとNode.jsのものが用意されている。

「お遊び組」って?

ISUCONは見学席が用意されていたのだけど、参加者以外はぶっちゃけ暇なので、空いたサーバ1チーム分を使って好きに遊んでいいことになっていた。で。そのお遊び用サーバで、事前に社内βで散々いじくり倒した俺とhidedenさんが空気読まずに自分のアプリで参加者達に対抗する、という大人気ないことをしていた。(すいません…)

一応ちゃんと言っておくと、中の人なので裏ルールや罠の存在についてある程度聞いていた上、6時間どころか2日3日費して試行錯誤していて、加えてhidedenさんに圧倒的な差を付けられた時は教えを乞うなどしていて、完全なるチートです。正直あの短時間でここまでやってくるって本当凄いな参加者のみなさん、と感動しっぱなしでした。勝手に熱くなって最後の方はムキになってたのは内緒です。

※ あ、でもでも、参加者の条件と対等じゃない「強くてニューゲーム」だった(会場に着くやいなや会社に行ってソース取って来た)という意味での「チート」で、コンテスト向けの実運用ではありえない実装にするとかはしてないです。なるべく突飛なことをせず普通にWAFを使い普通に配置するように心掛けてます。サーバのセットアップとかは手伝って貰ってるけど、アプリの実装は一人でやってるし、戦略そのものも自分で考えてます。

というわけで以下は俺のアプリで実際にやったこと。お遊び組ベストスコアを出した方とは別のアプリです。ちなみに使用言語はRuby。隣に座ったCTOと向かいに座った部長に「うちPerlの会社だからね?」とニヤニヤされながらも100%趣味全開のチョイス。「言語処理系の性能の違いが戦力の決定的差では無いということを教えてやる!」と赤くて速い人気取りで息巻いてみたものの、実際問題別な意味で全くその通りだった

糞クエリ対策とキャッシュ

殆どのチームがまずはクエリの見直しとDBのチューニングに手を付けてた様子。テーブル構造の見直しからMySQLのオプションやストレージエンジンの変更とかをやってたチームもあったみたいだけど、俺は必要なカラムにインデックスを貼る程度に留めて、データをガンガンキャッシュしてなるべくDBまで到達しないようにする戦略にすることにした。

まぁまずみんな真っ先に気付いてたところとしては、アプリ内に非常に残念なクエリがわざと仕込んであるということ。素晴しく分かりやすいお手本のような糞クエリだった。サイドバーのデータを取得するのが次のようなクエリ。

SELECT a.id, a.title
FROM comment c
LEFT JOIN article a ON c.article = a.id
GROUP BY a.id
ORDER BY MAX(c.created_at) DESC LIMIT 10;

記事が数千件、コメントが十数万件あるので、これは大分辛い。しかもサイドバーは全ページに表示されるので、全てのリクエストでこのクエリが発行されるという鬼畜さ。なので、次のように変更する。

  1. キャッシュからサイドバーに表示する記事IDのリスト取得を試みる
    1. なければ、commentテーブルから記事IDのみのリストを取得する
    2. キャッシュする
  2. 記事IDのリストを元に、キャッシュから記事データ10件のリスト取得を試みる
    1. それでも無かった数件をarticleテーブルからWHERE id INで取得する
    2. キャッシュする

あと、サイドバー・インデックスページ・記事詳細ページでそれぞれ必要なカラムだけを取って使ってたけど、これは逆にID・タイトル・本文・投稿日時全てを取得してIDをキーにキャッシュに突っ込むようにした。こうすることでサイドバーとインデックスと記事詳細でキャッシュを共有できるようになる上、コメントの投稿も記事の参照も新しい記事に偏っているので、サイドバーを読み込む時点では記事データはほぼ全てキャッシュに載っていることが期待できる。

commentテーブルから記事IDのリストを取得してるところはまだ重いと思うけど、当初のクエリよりは遥かに速いし、並行してコメントが投稿されたときの整合性の担保とかするのが地味に面倒だったので、とりあえず後回しにする。

コメントはコメントのIDではなく記事のIDでまとめてキャッシュするようにした。ページングや並べかえ、コメントの削除や編集などは仕様になく、コメントのパーマリンクなどもないので記事ページ以外では表示されないため、1回で全部取れるのがよかろうという判断。それらがあるようだったら、記事と同じようにコメントIDをキーにして「リストの順番や内容が変更されてもIDだけを取ってくるようにして、データ自体はキャッシュに載ってるものは使う」みたいにしたかもしれない。でもあんまりDBやmemcachedへのリクエストが増え過ぎるのはアレなので何らかのまとまりでキャッシュするかなぁ、とか色々考えてたけど、複雑になって余計悪化する、みたいなことにもなりかねないし、微妙に難しい。これもとりあえずこれでいいやってことにして後回し。

ノンブロッキングなフレームワーク

若干工夫してみたのは、フレームワークにはGoliathを選んだあたり。EventMachineベースのRubyのWebアプリケーションフレームワークおよびサーバで、これでI/Oを多重化して、同時接続数が増えてきても重いI/O処理でブロックしないで効率的に処理してくれることを期待する。まぁ、ぶっちゃけこれは実際にはそんなに効果なかった。

当初はPOSTのリクエストがばんばん飛んでくるとか、複数のテーブルに跨ってデータを取ってきて複雑な集約をするとか、画像アップロードみたいな長時間コネクション張り続けるようなリクエストがあるかなとか、そういう状況を想定してたんだけど、実際にはGETの比率のが圧倒的に高いしテーブル構造もシンプルで投稿はテキストのみ、みたいなパターンだったので「並列にI/O処理をする」ことがあまりなかった。

「遅延書き込みをするようにしてPOSTの際は即座にレスポンスを返してしまう」みたいなこともちょっとはやろうとしてたのだけど、「POSTリクエスト完了後1秒以内に表示に反映すること」ってルールにひっかかってテスト通らないことが頻繁にあったりして地味に嫌な感じになったので方針転換した。ここはむしろ同期処理にして「終わったら即書き込み即キャッシュ破棄」するようにして(実際には反映までにかかる時間に大差は無いはずなんだけど、こう振る舞った方がクライアント側からは速く反映されてるように見える)、POSTでは若干の時間をかけてしまってもいい、それよりもGETリクエストが来たときに既に準備が整ってるようにここで再取得再キャッシュまでやってしまう。

同時接続数の方はどうかというと、そもそもベンチマークスクリプトの並列数が最大10とかだったしコネクションも一瞬で切断されるし、じゃあワーカプロセスを10個立ち上げちゃえばいいじゃん、というオチが付いた。CPUもメモリもスッカスカでアプリサーバが遊んでたし、RubyやPythonみたいな言語を使う分には、1プロセス辺りの並列処理の効率化みたいなとこよりもUnicornの割り切りっぷりの方が現状理にかなってると思う。てことで言えばぶっちゃけ最初の素のSinatraで別に問題なかったような…どうしてもEventMachine使いたいならRainbows!Async Sinatraとかでも良かったような…まぁもう書いちゃったしいいか…。

余談だけどGoliath採用に併せて関連ライブラリも選び直すことになったので、MySQLクライアントにはMysql2を、mechachedクライアントにはremcachedを、ついでに趣味でテンプレートエンジンにSlimを使った。Mysql2やSlimは大分良かったので何か機会があれば使っていきたいところ。

Web「アプリ」って何?

とにかくもうDBに複雑なことさせたら負け、それ以前にDBに行ったら負け、と来たら次に来るのは「ていうかアプリに行ったら負け」。この段階ではフロントのWebサーバはアプリサーバ2台のロードバランサとしての仕事しかしていなくて、静的ファイルもアプリ側でファイル読んで返してたし、更新してないページも毎回アプリにリクエストが来ていた。バックエンドのアプリケーションサーバがどんなに速くなってもフロントエンドのWebサーバの処理速度とは文字通り桁が違うので、できればなるべくバックエンドに行って欲しくない。ならばということで、フロントエンドでページ丸ごとキャッシュしてGETリクエストは全部そっちで返してしまうことにする。

VCLの記述力やキャッシュ管理のしやすさ、ESI機能、あと名前のかっこよさなどが魅力的だったので、最初はVarnishを使ってみた。directorでアプリサーバ2台をまとめて、GET以外のリクエストは素通りするようにして、GETのレスポンスはページ単位でキャッシュするようにする。このままだと当然「投稿は1秒以内に反映されること」というルールが満たせないので、HTTPでパージできるように設定して、POSTのリクエストを処理したらアプリ側からVarnishにPURGEリクエストを送るようにした。

そうなると今度はキャッシュの破棄のタイミングが問題になってくる。記事の投稿はまぁいい。記事が投稿されて内容が更新されるのはインデックスページだけなので、1ページ破棄してやればいい。が、問題はコメントの方。コメントが投稿されるとまず記事ページが更新されて、「最近コメントが付いた記事10件」が表示されるサイドバーも更新される。が、このサイドバーは全てのページで表示されている。どこかの記事に1件コメントされる度に全ページのキャッシュが破棄されてたのでは殆どキャッシュの意味を為さない。なのでサイドバーは各ページのレンダリング時には生成せず、ESIでVarnishの段階でincludeさせるようにした。こうすれば、コメントが投稿されたときにパージするのは該当する記事ページとサイドバーだけになる。

そこまでやると最初の数回とPOST直後の数回以外は全部Varnishが返してくれるようになるので、アプリサーバの方は殆ど遊んでいる状態になる。前の段で「リソース余ってるからガンガンプロセス増やしちまおうぜ」って言えたのはこのおかげ。数万リクエスト処理しても数百とか数千くらいしか後ろに到達しなくて、さらにその後ろのDBまで行くのはもっと絞られてくる。この段階で初期状態から10倍くらいパフォーマンスが向上している。

ところがこの辺りで地味に嫌な罠を踏む。VarnishでESIを使うとContent-Lengthヘッダを返さなくなるので、Keep-Aliveで接続してるクライアントがいつレスポンスが終わったのかよくわからなくてタイムアウトするまで一部のクライアントできちんと扱えないのにKeep-Aliveのリクエストを送ってきたときにいつレスポンスが完了したのか判断できず、コネクションが切れるまで待ち続けてしまう。設定でどうにかできそうな気がするけど不慣れなVarnishに四苦八苦してなかなかうまくいかず、前段にもう一段Nginxを立ててリクエスト/レスポンスをいじってみたりするも今度は多段にしたのが祟ってそのオーバーヘッドで大分パフォーマンスが落ちてしまう。

結局Keep-Alive問題は真面目に対応するのをやめていかなる場合でも無効にしてしまおうかーとか考えてるあたりで、hidedenさんのNginx/SSI+SCGI構成にダブルスコアをつけられてしまって、NginxマジはえーほんとパネェつかVarnishってぶっちゃけNginxより遅いのに何で選んだの、みたいな声に負けてVarnishと戯れるのを放棄することにした。いや多分、俺のVarnish力の低さのせいで真の実力を発揮できてなかっただけでそこまでオワコンでは無いと信じたいのだけど、と一応擁護しておく。でもしばらくはNginx一択だけど。

ちなみに、ベンチマークスクリプトはそもそもKeep-Aliveに対応してないその「一部のクライアント」と同じ挙動をしていたわけではないので、この時点ではこの問題は割り切って無視するという選択肢もあった。が、本番当日の講評の際にkazeburoさんが恐しい罠を仕込んでたことを知らされる(っていうか社内では普通に話してたらしいけど聞いてなかった)。なんと、HTTP1.1じゃない持続的接続ができないのにそうできるかのように偽装してKeep-Aliveって付けた嫌がらせリクエストを3%程混ぜており、これに律儀に応えると、ベンチマークスクリプトはコネクション切れるまで待ち続けてしまって致命的に遅くなる、というもの。Nginxが素晴らしいのは周知の事実なので本番でも使ってくるチームが多いことを予想して、「Nginx(や、他の高速Webサーバやキャッシュサーバ)をチューニングせずにただ設置するとハマる罠」を仕掛けたんだとか。そうと知らずにそれを回避することに成功していて怪我の功名だった。本当運営の人達悪魔や…。

Nginxのチューニング

気を取り直してNginxの設定。まずNginxはWebサーバなので、餅は餅屋ということで静的なファイルはアプリサーバからフロントサーバに全部持ってきてNginxに返させてしまう。これでアプリから静的ファイルの配信機能を取っ払うのに成功して、本当に若干だけど無駄な処理を減らせる。リバースプロキシの設定は簡単なのでこれも普通に設定してしまう。それからVarinishのESI同様NginxでもSSIを有効にする。

それからキャッシュ。Nginxのキャッシュの方法はいくつかある。まず直感的なのはファイルキャッシュ。キャッシュファイルの置き場所を決めておいて、upstreamに飛ばすlocationのところでcacheを使うよって指定してあげれば、upstreamからのレスポンスを自動的にそこに貯めてってくれて、二回目以降は勝手にそのファイルから返すようにしてくれる。Varnishのときはキャッシュストレージにメモリキャッシュとファイルキャッシュが指定できて、ファイルの方を指定すると格段に遅くなるので、Nginxもそうなるんじゃないかと思ったけどこれが驚く程高速でびっくりする。むしろVarnishのメモリキャッシュの時より速いくらいだった気がする。ちゃんとは検証してない。ただ、このファイルキャッシュはuriをハッシュしてディレクトリに配置してしまうため、外からはどれが何のキャッシュなのか分からなくて、Varnishに比べるとキャッシュの管理が難しい、と思ってたら、ちゃんとこういうモジュール作ってる人がいた。これならVarnishのときに作ったHTTP越しのパージの仕組みがそのまま使える。

もう一つ、memcachedをまるでアプリサーバのように見立てて、pathをキーにしてmemcachedにあるデータをそのままレスポンスボディにして返してしまうという驚きのモジュールもある。こっちの利点は、アプリ側からもキャッシュが扱い易いということ。普通にアプリからmemcachedに繋いで、Nginxにレスポンスを返すときに同時にmemcachedにも書き込んでおくと、次はそっちから読んでくるようにしてくれる。破棄するのも普通にdeleteすればいい。HTTPリクエストを投げてパージするよりは分かりやすいし、memcachedプロトコルのがHTTPよりは速そうだ。が、問題は、前述のファイルキャッシュより遥かに遅いということ。これはmemcachedが遅いというよりmemcachedに毎回コネクションを張り直すコストがファイルキャッシュからの読み込み(これはおそらくかなり内部で最適化されているはず)のコストよりも高いせいらしい。ほぼ同じ状態でファイルキャッシュをmemcachedに切り換えたら、スコアが半分以下になってしまって絶望的な気分になった。仮にHTTP越しのパージよりもmemcachedのがアプリ側からは扱い易くてコストも低かったとしても、大半はNginxの段階で完結するキャッシュ済みGETリクエストなので、そっちのオーバーヘッドの方がもろに結果に影響する。ので最初はファイルキャッシュを採用した。

けど、hidedenさんの方はmemcachedを使っててそれでも俺のやつよりパフォーマンス出てるのでなんでだろうと聞いてみたら、upstreamとのコネクションを繋ぎっぱなしにしていたからだったらしい。試しにkeepaliveを設定してみたら、アプリ側何もせずに一気に3倍くらいのスコアになってhidedenさんのスコアに肉迫するレベルになった。同じことを当日もやらかした。hidedenさんが毎分11万リクエストというハイスコアを出した一方、前日まで大差はついてなかった俺の方は3万程度しか出てなくて焦ったのだけど、nginx.confを見直してkeepalive付け忘れに気付いて再起動したらちゃんと動いた。

で、結果はというと、ギリギリ100000req/minを越えるくらい。「お前はチートしてそれかよショボいな」と言われないくらいの結果は出せたと思うのでちょっとホッとした…。

この戦略の意味

POSTよりGETが圧倒的に多く、大半のリクエストが新しいデータに集中し、一度書き込まれた投稿はその後はあまり書き換えが起こらない、というのは、「大部分がほとんど書き換わらない動的コンテンツ」ではなくて「一部分が書き換わることがある静的コンテンツ」だと思っちゃえば少し話が簡単になる。

GETリクエストなんか静的なHTMLファイルを自動生成するためのトリガー、くらいに考えて、ただそれがファイル書き出しじゃなくてメモリ上のキャッシュに突っ込む方が扱い易いよねって発想で行けば、どんなミドルウェアが欲しいかとか、アプリは何をするべきなのかとか、どこで一番頑張るんだそうかフロントのWebサーバか、みたいなことでやることが決まる。てか、どっか1チームくらい本当にHTMLファイル書き出してデプロイしちゃうとこ無いかなーとか思いながら見てた。多分それはそれで速い。絶対面倒なのであまりやりたくは無いけど。

もっとぶっちゃけ話をすると、ライブドアブログの閲覧側チューニング戦略が(実現してる方法は違うけど)大体この形なので、ページのキャッシュと更新の局所化は最初からやる予定でいた。アプリエンジニアの性でついついアプリいじりに時間を割きたくなっちゃうとか、ミドルウェアやサーバ管理の知識経験が致命的に不足していたので時間ばっかり食ってしまったとか、ってのが「時間内には終わらなかったけど最終的には出来た」の実情だったりします。アプリエンジニアは常日頃からそういう知識をちゃんと収集しておけ、あとインフラチームと仲良くしておけ、色々捗るぞ、というお話でした。

上手く行かないケースと使い回せるテクニック

ブログ型の単純な表示系のリクエストが多いお題だったからWebサーバの性能が結果に直結してたけど、更新系のリクエストが多くて条件によって表示要素の個数や種類が大きく変わる、みたいな場合だと今度はアプリやDBの方に比重が移ってくる。例えばTwitterのサブセットみたいなのがお題だったらまた全然結果が違ったはず。

データのキャッシングやSSIみたいな仕組みはその場合でも有効だろうけど、「Nginx置いたらパフォーマンス20倍になったwwwwマジうめぇwwwwアプリ関係ないwwww」みたいなシンプルなことにはならないので、そっちの場合はアプリの実装力を鍛えてないと死ぬ。ISUCONに「部門別」とかあったら面白いかもねーとか思ったけど準備する側が死にそうなので軽々しく言うのはやめときます。僕お手伝いって名目なのに普通に遊んでただけでほんとごめんなさい。

反省点とか

DBロクに見てない。上位陣の方々見てるとMySQLバリバリチューニングしてるので、もっとちゃんといじればもう少し速度出るはず。一番効果が高いところを優先したと言えばそうだけど、あんま詳しくないところを放置しただけだったりもするので(アプリの全面書き直しとかマジで要らんかった)、ちゃんと勉強する。

みんなもやってみるといいよ

ISUCON運営チーム謹製のベンチマークツールと各言語の参考アプリは公開されてるので、是非触ってみてくだしあ。ゲーム感覚で楽しいし、各参考実装や意図的に仕込まれてる罠は、新人教育なんかにもうってつけだと思う。ええはい。自分の実力不足をガチで痛感した次第です。いや本当勉強しよう。ちゃんと。

faultierfaulist  at 19:13  | コメント( 0 )  | トラックバック( 0 )  | この記事をクリップ!

Objective-Cで正規表現を使う その2

前の記事で予告した通り、今度はNSRegularExpressionの話。

正規表現でマッチした部分文字列を取得する

まずNSRegularExpressionオブジェクトを作って、それのメソッドにNSStringのオブジェクトを渡す、という形で使う。まぁ説明するよりコード見た方が早い。

NSString *string = @"「そんな正規表現で大丈夫か?」「大丈夫だ、問題ない」";
NSError *error   = nil;
NSRegularExpression *regexp =
  [NSRegularExpression regularExpressionWithPattern:@"「そんな(.+)で大丈夫か?」「(.+)」"
                                            options:0
                                              error:&error];
if (error != nil) {
  NSLog(@"%@", error);
} else {
  NSTextCheckingResult *match =
    [regexp firstMatchInString:string options:0 range:NSMakeRange(0, string.length)];
  NSLog(@"%d", match.numberOfRanges); // 3のはず
  NSLog(@"%@", [string substringWithRange:[match rangeAtIndex:0]]); // マッチした文字列全部
  NSLog(@"%@", [string substringWithRange:[match rangeAtIndex:1]]); // "正規表現"
  NSLog(@"%@", [string substringWithRange:[match rangeAtIndex:2]]); // "大丈夫だ、問題ない"
}

地味にややこしい。Rubyで書いたらこんなんで済むのに。

# coding: utf-8
if "「そんな正規表現で大丈夫か?」「大丈夫だ、問題ない」" =~ /「そんな(.+)で大丈夫か?」「(.+)」/
  puts $&
  puts $1
  puts $2
end

まぁRubyやPerlと比べるのは(少なくとも文字列操作や正規表現に関して言えば)フェアじゃないですけど!とにかくこれで正規表現で部分文字列を探せるようになりました、と。

ちなみに、-firstMatchInString:options:range:というメソッド名で分かると思うけど、これは最初にマッチした箇所しか取ってこない。マッチした箇所全て欲しければ、-matchesInString:options:range:を使えば、NSTextCheckingResultが入ったNSArrayが返ってくる。別に返り値はずっと取っておく必要はなくて、単にマッチする毎になんか処理をしたいんだよ、ってときは、-enumerateMatchesInString:options:range:usingBlock:が使える。さっきの-firstMatchInString:options:range:を書き換えるとこんな感じになる。

NSRegularExpressionOptions options = 0;
NSRange range = NSMakeRange(0, string.length);
id block = ^(NSTextCheckingResult *match, NSMatchingFlags flag, BOOL *stop){
  NSLog(@"%d", match.numberOfRanges);
  NSLog(@"%@", [string substringWithRange:[match rangeAtIndex:0]]);
  NSLog(@"%@", [string substringWithRange:[match rangeAtIndex:1]]);
  NSLog(@"%@", [string substringWithRange:[match rangeAtIndex:2]]);
};
[regexp enumerateMatchesInString:string options:options range:range usingBlock:block];

Blocksの使い方は以前書いた記事とか読んでもらえると分かるかもしれない。あの記事を書いた時点ではiOS4.0を想定してアプリ作れなかったので実質まともに使えるのがSnow Leopardだけだったのだけど、今ならiPhone/iPadともに4系前提で作れるし、そもそもNSRegularExpression自体がiOS4.0以降にしか無いのでNSRegularExpressionを使える環境ならBlockも使えるので問題ない。

置換する

正規表現が使えるなら一番やりたいのは置換だろう、ということでもちろん置換もできる。-stringByReplacingMatchesInString:options:range:withTemplate:というのがそれ。

  NSString *string = @"「そんな正規表現で大丈夫か?」「大丈夫だ、問題ない」";
  NSString *template =
    @"$0\n→($2砕け散る)\n→「神は言っている、ここで死ぬ運命ではないと」\n→「$1」「一番いいのを頼む」";
  NSRegularExpression *regexp =
    [NSRegularExpression regularExpressionWithPattern:@"「(そんな(.+)で大丈夫か?)」「.+」"
                                              options:0
                                                error:nil];
  NSString *replaced =
    [regexp stringByReplacingMatchesInString:string
                                     options:0
                                       range:NSMakeRange(0,string.length)
                                withTemplate:template];
  NSLog(@"%@",replaced);

最初は話を聞かなかったあいつもちゃんと一番いいのを頼んできたので、今度は大丈夫だろう。しれっと$0とか$1とか使ってるけど、もちろんちゃんと置換文字列の中でキャプチャした部分文字列を参照したりできてるはず。

ただ、-stringByReplacingMatchesInString:options:range:withTemplate:は文字列そのものを置換してるわけじゃなくて、引数のNSStringのオブジェクトをcopyして置換したものを返してくる。なので、元のstringは何も変わってないので変わったつもりで使おうとしたらアレ?ってなるし、毎回文字列のコピーをするので場合によっては無駄になる。その場合は-replaceMatchesInString:options:range:withTemplate:の方を使う。基本的には-stringByReplacingMatchesInString:options:range:withTemplate:と同じなんだけど、以下の点が違う。

  • 引数にNSStringでは無くNSMutableStringを取る
  • 引数のオブジェクトのコピーではなく引数のオブジェクト自体を置換する
  • 返り値は置換後の文字列ではなく整数値で、置換箇所の数を返す

というわけで、ある正規表現で置換した文字列をさらに別な正規表現で置換して、みたいなことをやる場合はこっちのメソッドを使うべき。

ちなみに、上記二つのメソッドはマッチした箇所を全部置換する。例えば下のようなコードだと「大丈夫か」と「大丈夫だ、」が両方置換される。

  NSString *string = @"「そんな正規表現で大丈夫か?」「大丈夫だ、問題ない」";
  NSString *template =
    @"チョ☆チョニッシーナ☆まっソコぶれっシュ☆エスボグリバンバーベーコンさんだね!";
  NSRegularExpression *regexp =
    [NSRegularExpression regularExpressionWithPattern:@"大丈夫(か|だ)、?"
                                              options:0
                                                error:nil];
  NSString *replaced =
    [regexp stringByReplacingMatchesInString:string
                                     options:0
                                       range:NSMakeRange(0,string.length)
                                withTemplate:template];
  NSLog(@"%@",replaced);

もしマッチした箇所の内特定の部分だけを置換したい場合は、-firstMatchInString:options:range:とか-matchesInString:options:range:でNSTextCheckingResultのオブジェクトを取得しておいてから、-replacementStringForResult:inString:offset:template:を使う、みたいな感じになるかしら。ちょっと面倒な気もするけど。

RegexKitLite or NSRegularExpression

両方書いてみた感想で言うと、個人的にはRegexKitLiteのNSStringにメソッド生やしてくアプローチのAPIのが使い易いと思った。CoreFoundation使ってごりごり書いてるのでパフォーマンスも悪くないし、割と早い段階からBlocksに対応してたりとアクティブに開発されてるし、その気になればソース読めるし(まぁ、チラ見しては見たもののあんまり読む気にはならないのだけども)…とか考えると、既にRegexKitLiteを使ってるなら別に無理にNSRegularExpressionに乗り換える必要は無い気がしてくる。iOS4.0以前のバージョンもターゲットにするなら他に選択肢はないし、あと何故かNSRegularExpressionクラスはiOSにしか無くてMacOSXでは使えないという面白いことになってるので、iOSでもMacでも動くようなコードを書く場合もやっぱりNSRegularExpressionは使えない。

とは言えNSRegularExpressionの方はFoundationの一部なので、数カ所正規表現での置換を使いたいが為に外部のコード落としてきてプロジェクトに組み込んでlibicucoreに忘れずにリンクして…ってしないで済むなぁとか、万が一iOSの内部の実装が変わったりなんかの規約が変わったりしてもおそらく書き換えないで済むだろうなぁという多少の安心感とかはある。ので、これから作るアプリで、4.0以降のみをターゲットにしてる場合は、NSRegularExpressionを使って書こうかなぁなんて思ったりしてたり。

faultierfaulist  at 01:48  | コメント( 2 )  | トラックバック( 0 )  | この記事をクリップ!

Objective-Cで正規表現を使う その1

ちょっと前に書こうと思ってて忘れてたネタ。iOSアプリ内で正規表現を使ってごにょごにょしようと思ったらRegexKitLiteを導入するのが一番てっとりばやいのだけど、iOS 3.2以降はFoundation Framework内でも地味に正規表現が使えるようになってきてるのでメモがてら記事にしておく。

NSRegularExpressionSearch

Cocoaで文字列中に別な文字列が含まれているかどうかを知りたいときは、NSStringの-rangeOfString:というメソッドを使う。RubyのString#indexみたいな感じで、見付かった文字列がどこにあるかの位置を返してくれる。こんな感じ。

NSString *string = @"I love Udon.";
NSRange match = [string rangeOfString:@"Udon"];
if (match.location != NSNotFound) {
  NSLog(@"Found: %@",[string substringWithRange:match]);
} else {
  NSLog(@"Not Found");
}

これにもう少し細かく色々なオプションを指定できる-rangeOfString:options:というメソッドがあるのだけど、iOS3.2以上のバージョンだとこのオプションにNSRegularExpressionSearchというのが指定できるようになっている。実際に使うときはこう。

NSString *string = @"1日3食のうち4食はうどんを食べたいと思っている。";
NSRange match = [string rangeOfString:@"[0-9]+食" options:NSRegularExpressionSearch];
if (match.location != NSNotFound) {
  NSLog(@"Found: %@",[string substringWithRange:match]);
} else {
  NSLog(@"Not Found");
}

rangeOfString:に正規表現(の文字列)を渡せるようになってちょっと便利。書式はICU-comaptibleだそうだけど、RegexKitLiteもlibicucoreを使ってるので、RegexKitLiteを使ってた人は得に気にすることなく使えると思う。

これだけでも大分マシにはなったんだけど、さっきのサンプルコード見て分かる通り最初にマッチした部分しか取ってこれないし、もしかして-stringByReplacingOccurrencesOfString:withString:options:range:とかにも正規表現使えるのかなとwktkしたのだけど、「You can use this option only with the rangeOfString:... methods.」だそうで。マッチした箇所を全部取ってくるとか置換するとかは別な方法でやるようだ。

NSRegularExpression

さっきのはNSStringの文字列検索のオプションだったけど、正規表現そのものを扱うNSRegularExpressionというクラスがある。NSRegularExpressionSearchオプションは3.2以降であれば使えるけど、NSRegularExpressionクラスは4.0以降。つまり今までiPadでは使えなかったので、RegexKitLiteを置き換えるには至らなかった。

が。そろそろiPad版を含むiOS4.2がリリースされるので、ようやくiPadでも4系の機能が使えるようになるのです。弟の仇をトルノデス。ということで次回はNSRegularExpressionを使った文字列検索を記事にします。予告。

faultierfaulist  at 01:26  | コメント( 2 )  | トラックバック( 1 )  | この記事をクリップ!

RubyでMongrel2のハンドラを書いてみる

こないだ、と言っても2週間くらい前の話なんだけど、社内でZeroMQMongrel2の勉強会をやった。Mongrelと言えば、俺がRails(たしか当時1.2くらいだったと思う)で仕事してた頃にアプリケーションサーバとして使ってたけど、最近だとThinとかPassengerとかUnicornとかの人気に押されてついぞ聞かなくなったアレだよなぁ、なんでPerlの会社の勉強会でMongrelなんだろう、と思ってたんだけど、Mongrel2はもはやRailsのアプリケーションサーバじゃなくて、通信にZeroMQなるものを使った汎用的なWebサーバになってたらしい。

大分野心的なプロジェクトではあるものの、今はZeroMQもMongrel2も「とりあえず出た」って感じらしく、今年一杯くらいは地雷原を突き進む気がある人だけ触るといいんじゃないかなという話だった。プロダクションで使うようなレベルになるにはもうしばらくかかりそうだけど、今なら各言語の実装も追い付いてないようなので、遊んでおくなら今のうち。いずれMongrel2が大流行したときに「faultierさん、是非本を書いてください!」ってお願いされることを夢見て色々ごにょごにょしてみたよ。

準備

何はともあれZeroMQとMongrel2をインストールする。とは言っても、Getting Startedの通りにインストールするだけ。例に書いてある奴は若干バージョンが古いので、それぞれの最新版を取ってきた方がいいと思う。ちなみにZeroMQはhomebrewにもFormulaがあった。pyzmqは無かったので自分で作るなどした。

今回はRubyで試すので、RubyのZeroMQバインディングも入れておく。これは普通にgem install zmqで入るはず。Rackのハンドラの例の方はffi-rzmqを使ってるんだけど、なんか手元の環境で上手くインストールできなかったのでそっちは試してない。まぁ今回やることにはどっちがどうとかあんまり関係ないのでzmqの方でいいか。

そこまでできたら今度はMongrel2の設定を用意する。なんでCで書かれてるはずのMongrel2がやたらとPythonのライブラリ入れまくるんだろうと思ったんだけど、Mongrel2の操作にはm2shというPythonで書かれたスクリプトを使うかららしい。設定の仕方が面白くて、まずはPythonで書かれた設定ファイルを用意して、それをm2shでsqliteのファイルに書き出し、それを使ってMongrelが起動する、というようになってる。だから多分m2sh相当のものをPerlなりRubyなりで用意してしまえば、別にPythonは必要ないはず。まぁ、面倒なので大人しくm2shを使う。設定ファイルはこんな感じにした。

# m2test.py
from mongrel2.config import *

main = Server(
    uuid ="2f62bd5-9e59-49cd-993c-3b6013c28f05",
    chroot="./",
    access_log="/logs/access.log",
    error_log="/logs/error.log",
    pid_file="/run/mongrel2.pid",
    default_host="localhost",
    name="main",
    port=6767,
    hosts=[
        Host(
            name="localhost",
            routes={
                r'/m2test': Handler(
                    send_spec="tcp://127.0.0.1:9997",
                    send_ident="70D107AB-19F5-44AE-A2D0-2326A167D8D7",
                    recv_spec="tcp://127.0.0.1:9996",
                    recv_ident=""
                )
            }
        )
    ]
)
settings = {"zeromq.threads": 1}
commit([main], settings=settings)

Mongrel2のexamplesに入ってたのを参考にした。なんとなくわかると思うけど、hostsの中のHandlerってやつが今から作るハンドラと通信する為の設定になる。これが出来たら、

$ mkdir run log tmp
$ m2sh init -db m2test.db
$ m2sh load -db m2test.db -config m2test.py
$ m2sh start -db m2test.db -host localhost

とかすると、Mongrel2が立ち上がる。http://localhost:6767/m2testでアクセスすると裏のハンドラに処理が渡るはずだけど、まだ作ってないのでこの時点ではレスポンスが返ってこず、延々待たされる。

プロトコルを調べる

Mongrel2は内部にアプリケーションサーバを持つわけではなく、基本的にやることはZeroMQを使った通信をするだけ。上に書いた設定だと、ローカルの9997ポートと9996ポートにZeroMQのソケットが用意されて、ハンドラは9997ポートからリクエストを受けとり、処理したら9996ポートにレスポンスを送ってやるようにする。ちなみにこのソケットは別にUnixソケットのことではなく、ファイルを経由したりプロセス内通信したりネットワーク越しに通信したり色々できるらしい。詳しくはZeroMQを調べてみるといいと思う。

リクエストのメッセージは、「センダのID コネクションのID パス ヘッダの長さ:ヘッダ,ボディの長さ:ボディ」の形式で飛んでくる。実際にはこんな感じ。

70D107AB-19F5-44AE-A2D0-2326A167D8D7 2 /m2test 542:{"PATH":"/m2test","METHOD":"GET","VERSION":"HTTP/1.1","URI":"/m2test","PATTERN":"/m2test","Accept":"application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5","Accept-Charset":"Shift_JIS,utf-8;q=0.7,*;q=0.3","Accept-Encoding":"gzip,deflate,sdch","Accept-Language":"ja,en-US;q=0.8,en;q=0.6","Cache-Control":"max-age=0","Connection":"keep-alive","Host":"localhost:6767","User-Agent":"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.62 Safari/534.3"},0:,

んで、レスポンスは「センダのID コネクションIDの長さ:コネクションのID HTTP/1.1 ステータスコード ステータスメッセージ ヘッダ ボディ」という形式で返す。例えばokと返すだけのレスポンスならこんな感じ。

70D107AB-19F5-44AE-A2D0-2326A167D8D7 1:3, HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 2

ok

つまりハンドラが何をすればいいかというと、9997ポートから来た上の形式のメッセージをパースしてリクエストを判断し、処理結果を下の形式に加工して9996ポートに送ってやる、とこういうことです。

ミニマムなハンドラを作る

そしてこちらが調理済みのハンドラになります(料理番組風)。

#!/usr/bin/env ruby
# coding: utf-8

require 'zmq'

sid   = "70D107AB-19F5-44AE-A2D0-2326A167D8D7"
con   = ZMQ::Context.new
rsock = con.socket(ZMQ::UPSTREAM)
ssock = con.socket(ZMQ::PUB)

rsock.connect('tcp://127.0.0.1:9997')
ssock.connect('tcp://127.0.0.1:9996')
ssock.setsockopt(ZMQ::IDENTITY, sid)

loop do
  str = rsock.recv
  sender, conn_id, path, str = str.split(' ', 4)
  ssock.send "#{sender} #{conn_id.size}:#{conn_id}, HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 2\r\n\r\nok", 0
end

こいつとMongrel2を起動させといて、http://localhost:6767/m2testにアクセスすると、okとそっけない返事が返ってくる、というだけのハンドラ。「今夜は帰したくない」とか「結婚しよう」とかいうリクエストを投げても「ok」って返してくれます。まぁ「別れよう」でも「ok」って返ってきますけど。

上のコードだとreceiveしたメッセージのセンダIDとコネクションIDしか見てないけど、ヘッダとボディをパースすればRackアプリに渡すENVを作ることができるし、Rackアプリが返すレスポンスの仕様は決まってるのでそれをMongrel用のレスポンスメッセージに変換してやるのも簡単にできるわけで、そこまですればMongrel2と連携できるRackハンドラが作れる、というわけ。あとは、このハンドラ自身でリクエストを処理しなくても、スレッドを一杯作ってその中でRackアプリの処理をさせて、ハンドラ自身はプロセス内通信でリクエスト/レスポンスの中継役になってやるとかすれば、ワーカをがんがん増やせるとか、そんな風にもできる。

あと、Mongrel2とハンドラ間はZeroMQで通信してるだけなので、お互いが生きてるか死んでるか、何個あるのか一個もないのか、などについて何も感知しない。ので、急に負荷が上がったらプロセスやサーバを増やして緊急投入してもMongrel側の設定は変更する必要なかったりとか、デプロイ時にはもう一個ハンドラプロセスを立ち上げて起動し終えたら古いプロセスを殺すとかしてやればダウンタイム無しでデプロイできたりとか、するんじゃないのかな。多分。

あ、ちなみに今回作ったものはGistに置いといた。

faultierfaulist  at 16:12  | コメント( 0 )  | トラックバック( 0 )  | この記事をクリップ!

うどんげQuineに対抗して遊んでみた

「RubyKaigiが終わったら真面目にやろう」とか言ってたくせに、中々やる暇無くて放置してたら大変に分かりやすい作り方講座が出てしまった上、弾さんまで乗ってきてしまって完全にタイミングを逃したfaultierですこんばんは。

悔しいので対抗してみる

うどんげが出たならてゐもいてもいいだろう、ということでてゐ。AAは上記の記事同様こちらを使わせてもらった。初春もいいなーと思ったんだけど表示してみたら大き過ぎて自分のターミナルで表示できなかったのでやめといた。コードはこんな感じ。

# tewi.rb
eval$s=%w't=true;e="eval($s=join("<<34<<34<<",qw{$t=1;$s=~/"<<92<<"[[0-9,]+"<<92<
<"]/;$n=eval($&);$e=          "<<39<<"eval$s=%w"<<39<<    ".chr(39)."<<39<<($s+(
($s.length>1756)?"   ":"#"<<$    s.gsub(/[^0-9a-zA-   Z]/,      "")[0,(1755-$s.l
ength)]));e[-312,  311]=""<<39<<   ";$e.=          ("<<39<<"#"<   <39<<".substr(
join("<<39<<39<<  ",split(/[^0-9a-z              A-Z]/,$s)),0,(200  9-length($s)
))).chr(39)."<<  39<<".join"<<39<<";@o   =       map{$t=!$t;split(//  ,((!$t)?su
bstr($e,0,$_,"< <39<<39<<"):chr(32)x$_)         )}@$n;for(1..34){spli  ce(@o,$_*
81,0,chr(10))}  ;print(join("<<39<<39<<         ",@o).chr(10      )    )}))";o=[
101,10,24,4,40 ,3,8,4,18,3,4,6,33,2,13           ,3,6,10,1            2,3,30,2,1
7,14,17,2,27,  2,21,3,1,7,19    ,2                     ,              25,1,23,9,
21,2,23,2,23,  9,12,6,1,                           4                   ,23,1,23,
11,9,12,23,2, 13,4,2,2                   1                             ,1,14,23,
2,9,27,1,19   ,22,1,8         ,                  1    9   ,             1,29,20,
3,7,9,1,      18,1,4      ,         1        ,          3           ,    1,13,16
,6,6,6,      1,9,1,8                          ,1           ,    1    0,    1,11,
1,4,14,      6,7,26,                   2,1     1,1,4   ,1              ,4,   2,4
,12,6,7     ,19,3,5,             5,  3,2,14,3  ,3,1   0,5               ,8,13  ,
2,2,8,    2,4,3,3,15,          5,2    ,7    ,  4,11 ,10,3,              4,2,4,1,
2,4,    1,6,14,12,4,1      3  ,   6,1,     2,1,3,4,5,18,3,1,3   ,   1   ,3,12,3,
12,1   3,5,1,2,4,15             ,1,1, 11    ,10,3,14,12,6,1 ,           2,5,13,2
,1   ,1,1,6,12,2,16            ,11,7, 8,     13,2,1,7,13,1  , 1      8,10,8,6,14
,  1,2,10,10,1,20,1           2,5,3,1        2,1,6,13,7,2,  1       9,13,26,14,7
, 2,16,16,11,1,13,14          ,8,2,14,      16,13,7,5,15,1 0,          3,8,10,1,
8 ,25,10,1,4,13,27,23,            12,4,   2,12,5,1,1,1 ,8,1,1             3,17,1
0  ,1,3,5,1,12,14,4,8,             4,10,2,13,25,2,3,9,4,2,1,3              ,15,1
3,  33,4,5,1,4,3,20,                8,182].map{ |i|t=!t;((!t)              ?e.sl
ice  !(0,i):32.chr*                i)}.join;1.up       to(35               ){|i|
o[(i*   81)-1,0]          =        10.chr};puts(o)#ttrueeeva          l    sjoin
3434qwt1                           s920992nevale39evalsw39            chr3  939s
slength1     7 5        6             sgsub09azAZ01755s          l   ength e3123
1139e39              39su        bstr          jo             #t1s09nevaleevalsw
chr39tt  rue         eeva  l   sjoin3434qwt1s9             20992nevale39evalsw39
chr3939sslen    gth17 56sg   sub09azAZ01755slengt        he31231139e3939substrjo
in3939split09azAZs02009lengthschr3939join39omapttsplittsubstre03939chr32xnfor134
spliceo810chr10printjoin3939ochr10o10110244403841834633213361012330217141'.join

Gistにも置いてあります。はい。

Quineじゃない件

上のコードをコピペしてRubyに実行させると何やらコードを吐くので、それをさらにRubyに流し込んで実行させてやると…なんということでしょう!エラーを吐くではありませんか!Quineになってねぇじゃねーか、このド低能が!

# tewi.pl(tewi.rbの出力結果)
eval($s=join("",qw{$t=1;$s=~/\[[0-9,]+\]/;$n=eval($&);$e='eval$s=%w'.chr(39).'t=
true;e="eval($s=join(          "<<34<<34<<",qw{$t=1;$s=    ~/"<<92<<"[[0-9,]+"<<
92<<"]/;$n=eval($&)   ;$e="<<3    9<<"eval$s=%w"<<39   <<".      chr(39)."<<39<<
($s+(($s.length>17  56)?"":"#"<<$   s.gsub          (/[^0-9a-zA-   Z]/,"")[0,(17
55-$s.length)]));  e[-312,311]=""<<3              9<<";$e.=("<<39<<  "#"<<39<<".
substr(join("<<3  9<<39<<",split(/[^0-9   a       -zA-Z]/,$s)),0,(200  9-length(
$s)))).chr(39)." <<39<<".join"<<39<<";@o         =map{$t=!$t;split(//,  ((!$t)?s
ubstr($e,0,$_,"  <<39<<39<<"):chr(32)x$_         ))}@$n;for(1      .    .34){spl
ice(@o,$_*81,0, chr(10))};print(join("<           <39<<39<<            ",@o).chr
(10))}))";o=[1  01,10,24,4,40    ,3                     ,              8,4,18,3,
4,6,33,2,13,3,  6,10,12,3                           ,                   30,2,17,
14,17,2,27,2,2 1,3,1,7,                   1                             9,2,25,1
,23,9,21,2,2   3,2,23,         9                  ,    1   2             ,6,1,4,
23,1,23,1      1,9,12      ,         2        3          ,           2    ,13,4,
2,21,1,1      4,23,2,                          9,           2    7    ,1    ,19,
22,1,8,1      9,1,29,                   20,     3,7,9   ,1              ,18   ,1
,4,1,3,1     ,13,16,6             ,6  ,6,1,9,1  ,8,1   ,10               ,1,11  
,1,4,14    ,6,7,26,2,1          1,1    ,4    ,  1,4, 2,4,12              ,6,7,19
,3,5,    5,3,2,14,3,3,      1  0   ,5,8     ,13,2,2,8,2,4,3,3,   1   5   ,5,2,7,
4,11,   10,3,4,2,4,1             ,2,4, 1,    6,14,12,4,13,6, 1           ,2,1,3,
4,5   ,18,3,1,3,1,3,            12,3,1 2,     13,5,1,2,4,15  , 1      ,1,11,10,3
,1  4,12,6,1,2,5,13,           2,1,1,1        ,6,12,2,16,11  ,       7,8,13,2,1,
7, 13,1,18,10,8,6,14,          1,2,10,1      0,1,20,12,5,3, 12          ,1,6,13,
7, 2,19,13,26,14,7,2,16            ,16,1   1,1,13,14,8, 2,14,1             6,13,
7,  5,15,10,3,8,10,1,8,             25,10,1,4,13,27,23,12,4,2,              12,5
,1,  1,1,8,1,13,17,10                ,1,3,5,1,12 ,14,4,8,4,10,              2,13
,25,  2,3,9,4,2,1,3,                15,13,33,4,5,       1,4,3               ,20,
8,182]   .map{|i|          t        =!t;((!t)?e.slice!(0,i):3          2    .chr
*i)}.join                           ;1.upto(35){|i|o[(i*81)            -1,0  ]=1
0.chr};pu     t s        (             o)#ttrueeevalsjoi          n   3434q wt1s
920992ne              vale        39ev          al             sw39chr3939ssleng
th1756sg  sub         09az  A   Z01755slengthe3             1231139e3939substrjo
';$e.=('#'.su    bstr( join   ('',split(/[^0-9a-zA        -Z]/,$s)),0,(2009-leng
th($s)))).chr(39).'.join';@o=map{$t=!$t;split(//,((!$t)?substr($e,0,$_,''):chr(3
2)x$_))}@$n;for(1..34){splice(@o,$_*81,0,chr(10))};print(join('',@o).chr(10))}))

ええはい。良く見てもらえると分かると思うけど、実はこのコードは自分自身を出力してない。何を出力してるかと言うと、Perlのコードを出力している。んで、そのPerlのコードは何をするかというと、最初のRubyのコードを出力する。つまり、最初のコードは「『このRubyのコードを出力するPerlのコード』を出力するRubyのコード」で、後のコードは「『このPerlのコードを出力するRubyのコード』を出力するPerl」のコードになっていて、お互いがお互いの自分自身を相手に出力させるという、「人は一人では生きて行けないんだ」というメッセージ性を持った難読コードなわけです。まぁ嘘です。RubyもPerlもやられちゃったので、そのまま真似してもつまらなかっただけです。こんな感じで遊んでみるといいよ!

$ cat tewi.rb | ruby | perl | ruby | perl | ruby

何をしたの?

実のところ別になんということはなくて、やってることは基本的にはうどんげQuineと一緒。配列リテラルとjoinとevalを使ったQuineはPerlでもRubyでも全く同じ要領でできるので、自分自身を吐く代わりに相手のコードを吐くように書き換えると簡単に行ったり来たりできるようになる。

# 任意に加工できるRubyコードを出力する、
# 任意に加工できるPerlコードを出力する、
# 任意に加工できるRubyコードの例
eval$s=%w'puts("eval(join("<<39<<39<<",qw{print("<
<39<<"eval$s=%w"<<
39<<".chr(39)."<<39<
<$s<<39<<".chr(39)."<<39<<".join"<<39<
<")}))")'.join

あとは、二言語分のコードが一つのAAの中に入ることになるし、PerlとRubyだとリテラルがそっくりなのでデータ部は共有したいので生のデータを書きたい、となると流石に元のAAデータだと文字数の制限がキツいから、反転して白抜きのAAにした。ちなみに、てゐの方はうどんげのとはAAデータ持ち方は換えてる(元記事のロジックをPerlで実装するのが面倒だっただけ)。

あとはまぁ、出力時に尻尾に適当なゴミを付けて文字数調整してるんだけど、文字列リテラルの入れ子が簡単に崩れるので面倒臭いとかそんな程度。chr(39)濫用しまくり。あとドットがRubyのドットなのかPerlのドットなのか良くわかんなくなったりします。

まとめ

途中で「俺は一体何をやってるんだ」と思ったら多分負けなんだと思います。一度ベースができちゃうと後はデバッグと文字数の調整が面倒なだけで、割と簡単にできます。あと段々哲学的な気分になります。

faultierfaulist  at 19:06  | コメント( 0 )  | トラックバック( 0 )  | この記事をクリップ!

Web Storageで遊んでみた

本当に遊んでみただけだけど、例えばこんなの。

訪問回数
0
メッセージ
なんか多分エラってる

リロードする度に訪問回数がインクリメントされて、3の倍数の時には「奴」が疼き出し、5の倍数の時には「奴」が目覚めてしまうという、訪問回数カウンタとFizzBuzzを組み合わせたしょーもない何かです。まぁこれだけだと「そうですね…で?」みたいな話で終わりなんだけど、一応無駄にlocalStorageを使ってみてます。ちなみに「貴様は既に漆黒の闇に囚われている…」云々て表示されてる場合はlocalStorageが使えないブラウザなので、違うのが入ってたらそれで試してみてください。Firefox、Safari、Chromeあたりの最新版だと動いてるはず。多分。Operaも動いてるっぽい。

ろーかるすとれーじ?

Web Storageって何なのって人はHTML5.JPにある資料でも読んでもらうとして、まぁ大雑把に言うとブラウザにKey-Valueストレージが付いててなってJSから簡単に扱えるようになってるよって話。使い方は簡単。単に、こんな風にすればいいだけ。

localStorage.devilCount = 1;

これでlocalStorageにdevilCountってKeyで'1'って値が保存されて、同じドメインのページからだったらlocalStorage.devilCountって形で参照できるようになって、ブラウザが終了しても永続化されてるという。かんたん。リロードするだけじゃなく、試しにブラウザ閉じてもう一度この記事を見てもっても、ちゃんとカウントアップされてるのが分かるはず。べんり。この例は流石にしょーもなさすぎるけど、まぁ上手く使えば色々できるよね。

こんな風にアクセスすることもできる。

localStorage['devilCount'] = 1;
localStorage.setItem('devilCount', 1);
localStorage.getItem('devilCount');

消すときはこんな感じ。

delete localStorage.devilCount; // or localStorage.removeItem('devilCount');
localStorage.clear();           // 全て消す

ちなみに、localStorageに保存されてる値は文字列として保存されるので、文字列以外の値を入れたければJSONとしてシリアライズ/デシリアライズするとかして使う必要がある。上の例ではparseIntして数値として扱ったりしてる。

あと、localStorageはorigin単位で別のストレージになることに注意。ドメインやポートが変わるとlocalStorageも変わるので、例えばhttp://image.blog.livedoor.jpとかhttps://blog.livedoor.jpとかからはアクセスできないし、一方で別のブログ(例えばhttp://blog.livedoor.jp/faulist-mobile/とか)からはアクセスできてしまうはず。

せっしょんすとれーじ

同じものをsessionStorageで実装してみる。インターフェースは全く同じなので、ソース見てもらえれば分かるけど基本的にはlocalStorageとしてたものをsessionStorageに書き換えるだけ。

訪問回数
0
メッセージ
なんか多分エラってる

何回かリロードしたあと、一旦ウインドウを閉じる、別のウインドウで同じページを開く、などしてみると、違いが分かると思う。上のlocalStorageを使ってる方はブラウザ終了させようが別のウインドウで開こうがその度にインクリメントされるけど、こっちは「このウインドウが開かれてから閉じられるまで」の間だけ値が保存されるので、ブラウザを終了させると当然リセットされるし、別のウインドウで開くとまた1からカウントされる。

インターフェースが全く同じなのは楽でいいな。次は何やってみよう。コミュニケーションAPIかな。

補足

セッションストレージはブラウザ閉じたら消えるけど、ローカルストレージの方は消えないので、気持ち悪い場合はこの記事中の「訪問回数をリセットする」ボタンを押すか、SafariやChrome、Operaだと開発者向けのツールから消せる。もう一度記事見たらまた保存されちゃうけど。Firefoxだとどっから消すんだろうな。

あと、上記のコードを自分で書いて試してみるときは、Firefoxの場合はローカルファイルを開いても駄目で、Webサーバを介して見る必要がある。ローカルファイルとして開いた場合、ローカルストレージはリロードする度にリセットされてしまって値が保存されないし、セッションストレージは無効になっててエラーが出る。Chrome、Safariでは特に問題ない。

faultierfaulist  at 19:57  | コメント( 2 )  | トラックバック( 0 )  | この記事をクリップ!

iPadでキーボードの表示に合わせたViewのサイズ変更

どうも。「いらないって言ってたのに開発機をいじってたら欲しくなっちゃいましたの法則」が発動してiPadも先週結局買っちゃった僕です。

iPad買ったら当然JailBreakしてターミナルでコード書きまくってやる、と思ってたんだけど、いきなりmobileterminalが動かなくて躓く。ふぁっきん。とは言え自分で修正するとか新しく作るとかまでする気も起きず、あーそうだJSとかHTMLくらいだったらiPad上で書いたりデバッグしたりできるエディタあったよねーと思って、AppStoreで探してみたらちらほら見付かるも、コレ、というのはまだ無い様子。当たり前だけど、Syntax Highlightができるメモ帳程度なんだよなー。

エディタのViewを作る

ここで「タッチインターフェースを有効活用したコードエディタ」ってのを思い付いたら一時代築けそうなんだけど、別に今のところなんかアイディアがあるわけでもないので、誰かが作ればいいなー、もしくはアイディアくれるといいなーとか言うだけ言っておいて、とりあえずごく普通のテキスト入力画面の話をする。

例えば

  • 画面いっぱいにTextViewが表示されてて
  • Viewが表示されたらTextViewにフォーカスが当たるようにして
  • キーボードが表示されたらそれに合わせてViewをリサイズもしくはスライドさせて
  • キーボードが非表示になったらまたViewをリサイズもしくはスライドさせる

ようなごく普通のテキスト入力画面を作りたいとする。

TextViewにフォーカスを当てるのは簡単で、例えばViewControllerのviewDidAppearとかの中でTextViewのresignFirstResponderメソッドを呼んでやればいい。そうするとTextViewにフォーカスが当たってキーボードがせりあがってくる。キーボードのサイズは、UIKeyboard(Will|Did)(Show|Hide)Notificationていう名前でキーボードの表示、非表示の際に通知が飛ぶので、それをViewControllerで受けてViewをいじってやればいい(この記事とかが参考になった: 画面いっぱいのUITextViewがキーボードに隠れないようにする ? LANCARD.LAB|ランカードコムのスタッフブログ)。リンクの記事だとアニメーションさせてるけど、UIKeyboardDidShowNotificationを受けて単にself.view.frameの値を変えてやるだけでもそれっぽくなる。

あれ、Viewが消えた

iPhone/iPod Touchだとこれで上手く行くんだけど、実はiPadだとこのままだと上手く行かない。どうなるかというと、「日本語キーボードと英語キーボードを切り換えるとViewがどんどん小さくなる、消える」みたいなことになる。これ何でなのかなと思ったら、iPadだと日本語キーボードのときはキーボードの上に変換候補を表示するバーが出るのでその分だけ英語キーボードと表示領域のサイズが違うんだけど、表示領域のサイズが変わる度にUIKeyboard(Will|Did)ShowNotificationが通知される。前述のコードだとkeyboardWillShow:が呼ばれる度にキーボードの高さの分だけViewの高さを縮めてるので、連続でkeyboardWillShow:が呼ばれちゃうとどんどん小さくなってしまうという話。

これを防ぐには、単にself.view.frame.size.heightからキーボードの高さ分丸ごと引くんじゃなくて、UIKeyboardFrameBeginUserInfoKeyでキーボードの表示領域変更前の、UIKeyboardFrameEndUserInfoKeyで変更後のサイズが取れるので、その差分を取ってViewのサイズを調整してやる必要がある。例えばこんな感じ。

- (void)keyboardWillShow:(NSNotification *)notification {
  NSDictionary *userInfo = [notification userInfo];
  UIView *superview      = self.view.superview;
  CGRect beginRect       = [superview convertRect:[[userInfo objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue] fromView:nil];
  CGRect endRect         = [superview convertRect:[[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue] fromView:nil];
  CGRect superviewFrame  = superview.frame;
  CGRect viewFrame       = self.view.frame;

  viewFrame.size.height -=
    (beginRect.origin.y > superviewFrame.size.height) // キーボードが表示されてないときは origin.y が全体の表示領域より下になってる
    ? endRect.size.height
    : endRect.size.height - beginRect.size.height;

  // 他にもやることがあればごにょごにょ

  self.view.frame = viewFrame;
}

hideの方は連続で呼ばれることはないのでそのままでも使えるけど、これも「既に非表示になってたら処理をスキップする」とかちゃんと入れといた方がいいです。あと、iPhoneSDK 3.2からUITextFieldやUITextViewにカスタムキーボードを付けてあげることができるようになったけど、iPhoneではまずやらないと思うけどiPadでは「それぞれの入力欄にそれぞれのカスタムキーボードを設定してあり、しかも全部形が違う」みたいなこともやろうと思えばできる。そうなるとコードのカオス度が格段に上がるし、上記のコードだとまた残念なことになるのでご注意を。

faultierfaulist  at 16:26  | コメント( 0 )  | トラックバック( 0 )  | この記事をクリップ!

Objective-Cのデリゲートの話

Objective-Cで、参照先のオブジェクトから参照元に通知を送る方法。」を読んてで思ったんですが、若干冗長な気がする。

プロトコルを使う

プロトコルを使うんだったら、最初からそのプロトコルに適合するオブジェクトしかdelegateになれないようにしちゃった方がいいと思う。こんな感じで。

@interface Foo : NSObject {
  id<SampleDelegate> delegate;
}
@property (assign) id<SampleDelegate> delegate;
@end

こうしちゃえば、そもそもSampleDelegateに適合しないオブジェクトはdelegateになれないので、conformsToProtocolのチェックは必要なくなる。UIKitでも(例えばUITableViewのdelegateとか)大体そうなってますね。あと、ObjCはレシーバがnilの場合はメッセージ送信を単に無視するので、respondToSelectorとか送ってもYESが返らないのでこの場合はnitチェックもしなくていいと思う。だから、notifyObjectChangedの定義はこれで十分。

- (void)notifyObjectChanged {
  if ([self.delegate respondsToSelector:@selector(objectChanged:)]) {
    [self.delegate objectChanged:self];
  }
}

非形式プロトコル(カテゴリ)を使う

プロトコルがoptionalなメソッドしか規定してなくて、あるメソッドを定義したデリゲートがある場合はそれを呼ぶけど、そうでない場合は無視するかデフォルトの動作をする、っていうような場合は、そもそもプロトコルで規定しないでカテゴリで実現しちゃう手もある。

@interface NSObject (SampleDelegate)
- (void)objectChanged:(id)object;
@end

@interface Foo : NSObject {
  id delegate;
}
@property (assign) id delegate;
@end

ちなみにnotifyObjectChangedの実装は同じでいい。このタイプの例はNSURLConnectionとか。

こっちの利点はプロトコルの宣言がいらないので、既存のクラスや外部のライブラリのクラスのインスタンスでもdelegateにできること。それが何であるかはどうだっていいんだ、ただ送ったメッセージに答えてくれる何かでありさえすれば、みたいなときにはこの方法でもいい。

欠点は、プロトコルと違ってコンパイル時にdelegateが期待してるオブジェクトかどうかを解決できないこと。元記事の例みたいにoptionalなメソッドしか規定してない場合だと、プロトコルに適合してるからと言って期待してるオブジェクトかどうかははっきりしないので大差ないんだけど、仮にそうであってもプロトコル宣言を強制することによってデリゲートになり得るクラスを限定するってこともあるので、この辺はケースバイケース。

Key-Value Observingを使う

あと、「あるオブジェクトのあるプロパティが変更されたことを知りたい」っていう用途に限って言えば、Key-Value Observingを使う手もある。

@implementation Bar

- (void)test {
  Foo *f = [[Foo alloc] init];
  [f addObserver:self
      forKeyPath:@"hoge"
         options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld)
         context:NULL];
}

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
    // do something
}

@end

こういう風にしておくと、fのhogeプロパティの値が変更されたときに、BarのインスタンスのobserveValueForKeyPath:ofObject:change:context:メソッドが呼ばれる。その際、keyPathには@"hoge"が、objectにはfが、changeには{ old: /* 変更前の値 */, new: /* 変更後の値 */ }というDictionaryが、contextにはNULLが渡ってくる。

これの利点は、「Fooクラス側には全く手を加えなくていい」「あらゆるオブジェクトに対して、一貫したインターフェースで同じように設定できる」ということ。Cocoaのクラスだろうが外部のライブラリのクラスだろうが自分で作ったクラスだろうが同じように「あるプロパティの値が変更されたら教えてねー」っていう設定ができるし、参照先のオブジェクトが参照元のオブジェクトのことを気にする必要が無くなって関係性が緩くできる。Barの仕様を変更したくなってもFooをいじる必要はない。あと、delegateの場合と違って通知を受けとるオブジェクトが複数設定できるので、アプリケーションのあちこちが同時多発的に状態変化するみたいなことができる。

欠点は、「f.hogeに代入するか、[f setValue:obj forKey:@"hoge"]が呼ばれたときだけしか通知されない」ということ。なのでFooクラスの内部でインスタンス変数を直接弄って内部状態が変わったりしたときには通知されない。あとは、あらゆるオブジェクトに対して使えるので便利なんだけど、どのオブジェクトからの通知も必ずobserveValueForKeyPath:ofObject:change:context:を呼ぶので、沢山通知を設定するとobserveValueForKeyPath:ofObject:change:context:の中身が大分カオスなことになるので、ご利用は計画的に。

他には

NSNotificationを使う方法もあるけど、多用すると処理がどこでどうなるかわかりづらくなるので割と慎重に使った方がいいかも。まぁこれはKey-Value Observingでもそうなんだけど、NSNotificationの方がより汎用性が高いのでよりこんがらがり易くてよくハマる。

faultierfaulist  at 02:17  | コメント( 0 )  | トラックバック( 0 )  | この記事をクリップ!

Objective-Cのアクセサの話

scrollView.canCancelContentTouches = NO;
[scrollView setCanCancelContentTouches:NO];

って書き方があってこれみんなどういう使い分けしてるんだろうなぁ。。って思ってます。

セッター - poohtarouの日記

セッターというか、ドット記法の話かな。

ドット記法と普通のメソッドの使い分け

まぁ、まずはこんなクラスがあったとします。

@interface Book : NSObject {
  NSObject *title;
}
@property (retain) NSString *title;
@end

このクラスのオブジェクトを作ってtitleを設定/参照するコードはこんな感じです。

Book *book = [[Book alloc] init];
// ドット記法
book.title = @"Dynamic Objective-C";
NSLog(@"%@", book.title);
// メソッド
[book setTitle:@"詳解 Objective-C 2.0"];
NSLog(@"%@", [book title]);

これは上と下どっちのコードも同義。ドット記法でアクセスすると、setTitle:メソッドやtitleメソッドが呼ばれる。getter/setterを自動生成させないで自分で実装した場合もちゃんとそのメソッドを呼んでくれる。じゃあこんな場合はどうか。

id book = [[Book alloc] init];
book.title = @"Dynamic Objective-C";
NSLog(@"%@", book.title);

これはコンパイルエラーになる。id型のオブジェクトのメンバにドット記法でアクセスしようとすると怒られる。いやid型じゃなくてちゃんとクラスを明示して変数宣言すればいいじゃんて?NSArrayのobjectAtIndex:やNSDictionaryのobjectForKey:は返り値の型がid型ですよね。キャストすればいい?だって中に入ってるのがBookクラスのオブジェクトかどうかわかんないじゃん。isKindOfClass:で調べてからキャストする?いやいや、そのArrayの中にはGameクラスのオブジェクトも一緒に入ってて、GameクラスもsetTitleできるからそこは区別なく扱えた方が便利なんだよ!…みたいなこともあるわけで。そんなときはこう書くかなぁ。

id obj = [array objectAtIndex:0]; // Bookのオブジェクトが入ってると期待できるとする
if ([obj respondToSelector:@selector(setTitle:)]) {
  [obj performSelector:@selector(setTitle:) withObject:@"化物語 上"];
}

はいできた。これでBookクラスのオブジェクトをid型の変数で受けてもちゃんとsetTitle:できました。ついでにsetTitle:メソッドを実装してるオブジェクトであれば、Bookクラスと継承関係になくてもsetTitle以外は全然無関係のインターフェースを実装してようとも同じように扱えます。アヒルのように鳴くものはアヒルではなく隣のおばあちゃんでした。じゃなかった、アヒルのように鳴くものはアヒルです。ちなみに、上のコードは実は

id obj = [array objectAtIndex:0];
if ([obj respondToSelector:@selector(setTitle:)]) {
  [obj setTitle:@"本当は怖いグリム童話"];
}

って書いてもコンパイル通るし実行時にエラーも出ない。まぁ警告出るけどね。んで、こういう場合はプロトコルを定義してあれば(そしてobjがそのプロトコルに適合してれば)警告が出ない。

// こんなプロトコルがあるとする
@protocol Title <NSObject>
- (NSString *)title;
- (void)setTitle;
@end
...
id <Title> obj;
...
[obj setTitle:@"本当は怖いグリム童話"];

簡単にまとめると、明示的にクラス名を指定して変数を宣言してるときはドット記法で、id型で受けるべき時はメソッドでっていうのが一応の使い分けかなー。元記事の例の場合はscrollViewはUIScrollView型で変数宣言してて、そのコンテキストではUIScrollViewであることがはっきりしてるのでドット記法でいいと思う。具体的なクラスを想定しているわけではなくて、あるアクセサを持っている何かのオブジェクトっていう扱いをするときは、respondToSelector:とperformSelector:を使うか、プロトコルを定義しといてメソッド呼び出しするか、って感じになると思う。

Key Value Conding

ところで、ObjCにはドット記法とメソッド呼び出しの他にもう一つ、オブジェクトのメンバにアクセスする方法がある。しかもこれはプロパティとドット記法が導入されたObjective-C 2.0になる前からある。例えばさっきのBookクラスのオブジェクトに対してだったらこんなことができる。

[book setValue:@"イチャイチャパラダイス" forKey:@"title"];
NSLog(@"%@", [book valueForKey:@"title"]);

恐ろしいですね。まるでオブジェクトがDictionaryか何かのよう。これはNSObjectの子孫にあたる全てのクラスで使える。そしてこれの恐しいのは、アクセサメソッドやプロパティが宣言されてなくても使えてしまうということ。例えばvalueForKey:@"title"だったら、

  1. titleメソッドが定義されてたらそれを呼ぶ
  2. titleメソッドが無くて、getTitleメソッドが定義されてたらそれを呼ぶ
  3. どっちも無くて、インスタンス変数titleが宣言されてたらそれを返す
  4. どれも無くて、インスタンス変数_titleが宣言されてたらそれを返す

っていう順番で解決されるので、Bookクラスの場合titleをプロパティとして宣言してる、つまりtitleメソッドが定義されてるのでそれが呼ばれる。もしプロパティとして宣言してなくても、title変数があるので3番目でひっかかってそれが返る。ちなみにインスタンス変数が@privateで宣言されてても関係ない。便利なんだけど怖い。1、2はともかく3、4は大分アレげなので、内部でしか使わない意図せず変更して欲しくないインスタンス変数は@privateで宣言した上でname_とかして上のルールにひっかからないようにしておく癖を付けたほうがいいと思う。外部からアクセスしてもいいやつでも名前は変えた上で明示的にプロパティで宣言した方が安全かなー。

元記事の話とは直接関係無いけど余談でした。なんでそんな仕組みがあるのってあたりは気になる人は「Key Value Coding」とかで調べてみるといいと思う。これはこれで便利だし、AppKitとかUIKitとかでは結構使われてるので。

追記

そうだブコメで指摘をもらってたんで追記しようと思ってたのすっかり忘れてた。iPhoneアプリでKey-Value Codingを一番使うであろう場面はCoreDataのNSManagedObjectですね。CoreDataでエンティティを定義してデータをつっこんだり取得したりするときは、NSManagedObjectかそれを継承したクラスがモデルオブジェクトになる。サブクラスを作るときはプロパティを定義しとけばいいんだけど、NSManagedObjectにはもちろん中のデータへのアクセサは定義されてない。じゃあどうするかと言うと、valueForKey:とsetValue:forKey:で値の取得や変更をするってことになる。

あと、これの次の記事で書いたけど、Key-Value Observingって仕組みがとても便利なので、プロパティについて調べたらついでにKey-Value CodingとKey-Value Observingについて調べておくといいと思います。まる。

詳解 Objective-C 2.0詳解 Objective-C 2.0
著者:荻原 剛志
販売元:ソフトバンククリエイティブ
発売日:2008-05-28
おすすめ度:4.0
クチコミを見る
Dynamic Objective-CDynamic Objective-C
著者:木下 誠
販売元:ビー・エヌ・エヌ新社
発売日:2009-03-27
おすすめ度:4.5
クチコミを見る
faultierfaulist  at 02:09  | コメント( 1 )  | トラックバック( 1 )  | この記事をクリップ!

MongoDBとCandy

そろそろRuby会議もあるというのに、そういや最近Ruby全然書いてないfaultierですこんばんわ。じゃあリハビリを兼ねて久々にRamazeさんで遊んでみるか、ついでにいい加減NoSQLブームにも乗ってみるか、みたいな感じでRamaze+MongoDB+Candyで遊んでみることにした。ちなみに社内では今Cassandraがブームなのだけども、Cassandraはちょっと遊びで使ってみるにはオーバースペックだよなーとか思いつつ色々見てたら、HerokuでMongoDBが使えるらしいのでそれを狙ってのMongoDBいじり。

とりあえず入れてみる

aptで探したらmongodbのパッケージもあるんだけど、2010年5月30日時点では1.2.2と若干バージョンが古い。この後オブジェクトマッパーを色々試してみたところ1.4系じゃないと動かなかったりしたので、本家のサイトから最新のバージョン落としてくる。コンパイル済みのパッケージなので展開して適当なとこに置いとくだけでOK。手元の環境はUbuntu 10.04の64bitなのでLinux 64bitってやつを選んだけど、OSX 32/64 bitとかWindows 32/64 bitとかもあった。

$ mongo
MongoDB shell version: 1.4.3
url: test
connecting to: test
type "help" for help
>

ふむ。繋った繋った。このあとチュートリアルとかにある例通りちょこちょこいじってみたけどちゃんと使えるようだ。

Rubyから使う

Rubyのdriverはgemからインストールできるので普通にgem install mongoとかやる。依存モジュールでbsonてのが入るけどなんじゃろ、と思ったんだけど、RubyのオブジェクトをMongoDBのBSONオブジェクトにシリアライズするモジュールだけ別のパッケージにしてあるらしい。んで、bsonはC拡張があって、そっちを入れてないとこんなメッセージが出る。

rb(main):001:0> require 'mongo'

**Notice: C extension not loaded. This is required for optimum MongoDB Ruby driver performance.
  You can install the extension as follows:
  gem install bson_ext

  If you continue to receive this message after installing, make sure that the
  bson_ext gem is in your load path and that the bson_ext and mongo gems are of the same version.

=> true

まぁローカルの開発環境だと別にどっちでもいいんだけど、サーバに置くときはどうせ使うだろうしbson_extも入れておく。

# coding: utf-8
require 'mongo'

con   = Mongo::Connection.new # 何もオプション指定しないと localhostの28017ポートに繋ぐ
db    = con.db('candy')
udons = db.collection('Udon')
udons.insert({
  'name'  => 'ぶっかけ',
  'type'  => '冷たいうどん',
  'price' => 350
})
udons.insert({
  'name'  => 'かけ',
  'type'  => '温かいうどん',
  'price' => 300
})
udons.insert({
  'name'  => '釜玉',
  'type'  => 'かまあげうどん',
  'price' => 400
})

p db.collection_names  #=> [ 'system.indexes', 'Udon' ]
p udons.count          #=> 3
p udons.find_one       #=> {"_id"=>{"$oid"=>"4c0273712e119e3fd7000001"}, "name"=>"ぶっかけ", "type"=>"冷たいうどん", "price"=>350}
p udons.find('price' => {'$gte' => 350}).count #=> 2

こんな感じでさくっと使えた。ほー。あとはMapReduceとか試してみようかと思ったけどまぁそれはおいおい。

オブジェクトマッパー

ぶっちゃけmongo-ruby-driverだけでも十分色々できるし、O/Rマッパー…あーいやRelationじゃないから、O/Dマッパーか、は無くてもいいかなと思ったけども、Ramazeとかで使うことを考えたらやっぱりあった方がいい(工夫してやれば既存のヘルパーとか使えそうなので)。んでちょろっと見てみたら、結構色々あるんだけど、どれもARっぽいんだよなぁ。なんというか、ちょっと過剰なのでもっと薄いのがいい。そしてMongoMapperもMongoidもActiveSupportとかvalidation用のライブラリとか入れちゃう。Railsで使うんならいいんだけど、というかRailsでARの代わりに使うのを想定してるっぽくて(ActiveModelってのがあるんだね。最近Rails全然見てないので知らないんだけどRailsもModel抽象化の流れなんかな)、悩ましい。つーかActiveSupportは入れないで欲しいなぁ…。

個人的には一覧の最後にあったCandyが良さげに見えた。なにしろ「ゴールはActiveRecordやDataMapperのミラーじゃない」ってREADME冒頭で明言してるし、mongo-ruby-driver以外には依存してないのが素敵。使い方もCandy::Pieceをincludeするだけとシンプル。

# coding: utf-8
require 'candy'

class Udon
  include Candy::Piece
end

kamaage = Udon.new
kamaage.name = 'かまあげ'
kamaage.type = 'かまあげうどん'
kamaage.price = 350

ちなみに、何も指定しなければこれでlocalhostの28017ポートに繋いで、candyというデータベースのUdonコレクションにアクセスする。つまりこのコードだと、一個前のコードと同じデータベースの同じコレクションから取ってくることになる。これはCandyクラスかUdonクラスのクラスメソッドで参照/変更できる。Candyクラスで設定すると全てのデフォルト設定になるし、クラスメソッドで設定するとそのクラスのデフォルト設定になる。Ramazeだったら、model/init.rbあたりでこう書いとく。

# coding: utf-8

require 'candy'

Candy.host = "mongdb.example.com"
Candy.port = "24423"
Candy.db   = "udonapp"

require __DIR__('udon')

Udon.collection = 'menrui' # クラス名じゃないコレクション名にしたい場合

あとsaveメソッドはない。なんかこれもポリシーらしくて、オブジェクトを操作したら即時反映される。method_missingでフックして随時更新をかけてるので、もしvalidationとかしたい場合は、例えばpriceが数字でなければならなくて、1000円超えるようなお高いうどんは売っちゃいけないポリシーにするとしたら、こんな感じのメソッドを生やしてやる。superが肝。

class Udon
  include Candy::Piece

  def price=(val)
    raise '値段は数字だっつってんだろ' unless val.kind_of?(Integer)
    raise 'たけーよアホか' unless val < 1000
    super
  end
end

ちなみにバリデーションとかエラーハンドリングとかnamed scopeとかの簡単な実装例はCandyのドキュメントに載ってるので見てみるといいと思う。まぁ若干面倒ではあるけど、正直こんなのでいいよなーと思った。しばらくこれで遊んでみよう。

…というあたりまで書いて大変致命的なことに気付いたんだけど、Candyは1.9系じゃないと駄目らしいんだけどHerokuってRuby 1.9系だっけ…?多分違うよね…?うーん、まぁ、ちょうど借りてるサーバもリビルドしたことだし、とりあえずそのサーバで動かせればいいか…。

faultierfaulist  at 00:01  | コメント( 2 )  | トラックバック( 0 )  | この記事をクリップ!

GCDを試してみる

Rubyと比べながらBlocksをいじってみたりBlocksでNSArrayにmapメソッドを生やしてみたりしてきたので、そろそろGrand Central Dispatch(GCD)も試してみる。あんま関係ないけど、グランド・セントラル・ディスパッチってなんか必殺技っぽいよね。じゃあ一緒に高らかに叫んでみようか。せーの、グランド!セントラル!ディスパッチ!!

GCDってなにさ

ドキュメント嫁。

…だけだと流石に不親切なので、一応簡単に説明すると、APIを通してぽんぽん処理をqueueにつっこんでってやると、ランタイムの方でそれを上手いこと並列実行しといてやるよ!安心しろチェリーボーイ共、スレッドのことは俺が面倒見てやるぜ!って仕組み。そんな口調なのかどうかはわかんないけど、まぁ大体そんな感じ(適当)。

例によってRubyと比較

まぁ、こんなコードがあったとします。

# ruby
f = lambda {
  puts "0.25秒後から本気出す"
  sleep 0.25
}
t = Time.now
20.times do
  f.call
end
p Time.now - t

んで、それを例によってObjCでBlocksを使って書くとこんなコードになります。ちなみにどっちも別にBlocksやlambdaでやる必要は無いんだけど、この後のコードと比較の為にわざとそうしてるのでスルーしておくれやす。

# ObjC
void (^f)(void) = ^{
  NSLog(@"あと0.25秒だけ寝させてー");
  [NSThread sleepForTimeInterval:0.25];
};
NSDate d = [NSDate date];
unsigned int i = 20;
while (i--) {
  f();
}
NSLog(@"%f", [[NSDate date] timeIntervalSinceDate:d]);

まぁ見ての通り、標準出力に一言言ってから0.25秒sleepするだけの簡単なお仕事を20回ほどやってもらってます。あたり前のことだけど、0.25*20で5秒+αくらいの時間がかかるし、0.25秒っつってんのに5秒待たせるとか相当いい加減なやつだ。

んで、こんな風に、それぞれの処理が独立してるけど一個一個は結構時間かかる、みたいなのは、並列に実行させちゃったらいいんじゃね、みたいなことを偉い人は言いました。

# ruby
f = lambda {
  puts "0.25秒後から本気出す"
  sleep 0.25
}
t = Time.now
20.times do
  Thread.new { f.call }
end
(ThreadGroup::Default.list - [Thread.current]).each{|th|th.join}
p Time.now - t

まだ若干怠けてるけど、まぁ0.3秒行かない程度で終わる。5秒に比べたら一瞬みたいなもんだよね。

じゃあ次はそれをObjCで…と言いたいところなんだけど、ObjCで上のRubyのコードをNSThreadってクラスを使って書こうとすると、割と面倒い。特定のコンテキストを別スレッドで実行しようと思うと、detachするのにtargetとselector、つまりスレッドで実行されるオブジェクトとそいつから呼び出されるメソッドがなくちゃいけない。んでもって、作ったスレッドを自分で管理しなきゃいけない。一応適当なサンプルは書いたけど、あんまりこれをObjCで自前で書くことは無いと思う(理由は後述する)。

さて、GCDです

最初の方に言ったけど、大分ざっくり言うとGCDってのは「処理のブロックをキューにつっこんでってやると裏で上手いこと並列に処理してくれる」ものです。要は並列処理のめんどい感じを多少楽にしてくれるのがGCDの兄貴だってことです。兄貴なのか姉貴なのかは知りませんが。どっちかというと僕はお姉さんが好きですがどうでもいいことです。

# ObjC
dispatch_block_t block = ^{
  NSLog(@"あと0.25秒だけ寝させてー");
  [NSThread sleepForTimeInterval:0.25];
//  NSLog(@"%@", [NSThread currentThread]);
};
//NSLog(@"%@", [NSThread currentThread]);

NSDate d = [NSDate date];

// ここからGCD登場
dispacth_group_t group = disptach_group_create();
disptach_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
unsigned int i = 20;
while (i--) {
  disptch_group_async(group, queue, block);
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
// ここまでGCDのお仕事

NSLog(@"%f", [[NSDate date] timeIntervalSinceDate:d]);

はい。Rubyの方をThreadを使って書き直したときと同じく、0.3秒行かないくらいの時間でさくっと処理してくれました。コメントアウトしてるのを戻せば、ちゃんと別々のスレッドで動いてるのも確認できると思います。ちなみにObjCの文法で書いてるとこを削れば普通にCでも使えます。使いたいときは#include <dispatch/dispatch.h>してください。

変わったところを解説すると、まずブロックの型がdispatch_block_tに変わってる。これは後で出てくるdispatch_group_asyncの引数の型なんだけど、void (^)(void)、つまり何も取らず何も返さないブロックって定義になってるので実はさっきと何も変わってない。

次にdisptach_group_createをしてgroupを作ってるけど、これはまぁ名前通り非同期に実行する処理をグルーピングするためのもの。Rubyの方でもThreadGroupが出てきたけど、あれと一緒で最後のdispatch_group_waitで一連の処理が全部終了するまで待ってやる為に使う。今回は全部の処理が終わるまでの時間が見たかったのと、アプリケーションとかじゃない普通のCUIのコマンドとして作ったときに何も考えずに非同期処理させるとdispatchしたのが終わる前にmainが終わっちゃうのでwaitする必要があったけど、GUIアプリケーションとかデーモンとかだとその心配はないのでグルーピングせずに単にdispatch_asyncしちゃってもいい。

んでここからが本質、dispatch_get_global_queueとdispatch_async(またはdispatch_group_async)。といっても別に大したことではなくて、

  1. queueを用意します
  2. dispatch_asyncにqueueとblockを渡します
  3. あとは裏でよしなにやってくれます

以上。中では「システムの負荷を見てスレッドを作るか待つか決める」「スレッドが一個も空いてなければ作るけど、さぼってるやつがいたら再利用する」「あっちこっちから放り込まれたブロックをどのスレッドに割り当てたら効率良いか考える」とか色々やってんだけど、使う側としてはそんなこと気にする必要無いし、それどころかスレッドが作られてることすら隠蔽されてる。やったのは単に関数にブロックを渡しただけ。ゆとりの僕でもできる簡単なお仕事です。ちなみにdispatchしてwaitするあたりの処理を続けて何回も実行すると、ちゃんとスレッド再利用してるのが確認できる。

他にも、globalって名前が付いた関数があるからにはglobalじゃないqueueを作る関数もあるとか、asyncって名前が付いた関数があるからにはsyncして実行する関数もあるとか、メインスレッドで動作するqueueがあるとか、さっきのコードではわざわざforループ回したけどループにはループ専用のdispatch_applyがあるとか、まぁ色々あるんだけど、Xcodeのあのアホみたいに使い辛いドキュメントビューワの検索窓にdispatch_って入れてやるといっぱい出てくるので見てみてくれればいいかと思います。

でもそれCじゃん

ええ。ここまでは誰がどうみてもCの関数、っていうかさっきも書いた通り実際<dispatch/dispatch.h>をincludeすればCでも使えるAPIです。いやさ、確かにObjCの文法はキモいけどさ、せっかくObjCで書いてるのにCの関数使うってどうなのよって?ご心配なく。ObjCならObjC流に実装する方法はもちろんある。

「並列実行」「どんどんキューにつっこむ」「スレッドの面倒はキューが見てくれる」あたりで、LeopardまでのOSXとかiPhoneSDKとかでアプリを書いたことある人は「それNSOperationとNSOperationQueueでできるじゃん」と思ったはず。これもキューにオペレーションオブジェクトをどんどんつっこんで行けば非同期でよしなにやってくれるクラスで、それ自体はGCDやBlocksとは関係なく前から使える。ので、もともとあんまり自前でNSThreadを作ったり管理したりはやったことなかった。じゃあObjCだと何も変わらないじゃんと思ったけど、Blocksが導入されたことでNSArrayやNSDictionary同様便利な機能が追加されているという。

# ObjC
void (^block)(void) = ^{
  NSLog(@"あと0.25秒だけ寝させてー");
  [NSThread sleepForTimeInterval:0.25];
//  NSLog(@"%@", [NSThread currentThread]);
};

//NSLog(@"%@", [NSThread currentThread]);
NSDate d = [NSDate date];

// こっからNSOpera(ry
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
unsigned int n = 20;
while (n--) {
  [queue addOperationWithBlock:block];
}
[qeueu waitUntilAllOperationsAreFinished];
// ここまでNSOpera(ry

NSLog(@"%f", [[NSDate date] timeIntervalSinceDate:d]);

おお、これは便利だな。Blocsを使わない場合は事前にNSOperationクラスを継承して独自のOperationクラスを作っとくとか、NSInvocationOperationを使うとかしてたところを、-[NSOperationQueue addOperationWithBlock:]を使えばブロックを渡してやるだけで非同期実行できちゃう。ブロックをひとまとめにしてオペレーションにしてくれるNSBlockOperationってクラスや、オペレーションが終了した後に実行される処理をブロックで設定できる-[NSOperation setCompletionBlock:]ってメソッドが追加されてて、「処理を一括りにして並列実行する」コードが大分書きやすくなっている。

MacRuby

RubyとObjCの話をしてるのにMacRubyさんを完全にスルーするという素敵なプレイをやってのけてきたわけですが、実はMacRubyさんはとっくにGCDに対応してやがります(MacRuby » An Introduction to GCD with MacRuby)。凄いな、ほんと、どこまで行くんだろう。

ついでに

Blocks入門NSArrayにmap生やしたのとこの記事のコードをのサンプルはgistに上げてみたので、まぁ一応一通り動く例になってるはず。あーあと簡易ベンチマーククラスみたいのも作ってみたので入れといた。適当なので実用するのはおすすめしません。Snow Leopardでrubyとrakeが入ってる環境なら、cloneしてきてrakeすればコンパイルできるはず。

参考記事

faultierfaulist  at 13:58  | コメント( 0 )  | トラックバック( 0 )  | この記事をクリップ!

BlocksでNSArrayにmapメソッドを生やしてみる

RubyエンジニアのためのObjective-C Blocks入門に引き続き、Blocksネタ。そっちの記事ではBlocksはクロージャ的ななにかだって言ってるのに単なる関数ポインタみたいにしか使ってなかったので、せっかくなのでクロージャ的に使ってみる。

eachできるならmapも欲しい

-[NSArray enumerateObjectsUsingBlock:]を使えば、Array#each相当のことができるとこまでは前回の記事でできた。そうすると、Array#map相当のこともNSArrayにさせられるはず。例えばこんなの。

# ruby
array_a = %w(ひたぎ 真宵 駿河 撫子 翼)
array_b = array_a.map {|x| "#{x}が可愛過ぎて生きるのが辛い" }

Rubyistには説明の必要もないと思うけど、Array#mapが何をしてるかというと、

  1. 1引数のブロックを受けて
  2. 自分自身の要素を一つずつブロックに渡して
  3. その返り値を要素にした新しいArrayを作って返す(※自分自身は変更しない)

みたいなところ。さてそれをObjCで実装する。まず、ベタに書くとこんな感じ。

// ObjC
@interface NSArray (Map)
- (NSArray *)mapUsingBlock:(void (^)(id))block;
@end
@implementation NSArray (Map)
- (NSArray *)mapUsingBlock:(void (^)(id))block {
  NSMutableArray *newArray = [NSMutableArray array];
  for (id item in self) {
    id obj = block(item);
    [newArray addObject:obj];
  }
  return array;
}
@end

これはこれでなんかスッキリしてていいんじゃねって気がしてきた。でもせっかくなので、enumerateObjectsUsingBlock:を使って書き直してみる。

// ObjC
- (NSArray *)mapUsingBlock:(void (^)(id))block {
  NSMutableArray *newArray = [NSMutableArray array];
  [self enumerateObjectsUsingBlock:^(id item,NSUInteger idx,BOOL *stop){
    id obj = block(item);
    [newArray addObject:obj];
  }];
  return array;
}

ちょこっと説明する。ブロックは、RubyやPerlのそれと同じく、生成されたコンテキストにある変数を参照できる。上のコードで言うとnewArrayはブロックの中じゃなくて外で宣言されてるけど、ブロックはその時点でのコンテキストを持ってるので、ブロックを生成したスコープで見えるものはブロック内でも同じように見える。

で、使う側ではこんな風に書けるようになる。

// ObjC
NSArray *array = [NSArray arrayWithObjects:@"ひたぎ",@"真宵",@"駿河",@"撫子",@"翼",nil];
NSArray *newArray = [array mapUsingBlock:^(id item){
  return [NSString stringWithFormat:@"%@ー!俺だーッ!結婚してくれー!",item];
}];

おお。それっぽいそれっぽい。

__block

ちょうどタイミング良く昨日弾さんとこで同じような話をしてたんだけど、上の例はしれっとブロックの外の変数に破壊的操作をしてるけど、例えばこういうのはできない。

// C or ObjC
typedef int (^bint)(void);

bint make_incr() {
  int n  = 0;
  bint f = ^int(void){ return n++; };
  return f;
}

ObjCのBlocksの実装だと、何も指定してないとブロックの外の変数は「見える」だけで「変更できない(ブロックの中から再代入できない)」。でも__block修飾子を付けて変数を宣言すると、ブロックの中から変数を変更できるようになる。それから、そのままfを返しちゃうと、関数のスコープ抜けたときにfが消えちゃうので呼び出し側で使えないところに注意する必要がある(試しにやってみたら、実際には最初の一回だけ実行できたものの、二回目を実行しようとしたところでbus errorになった。ブロックが解放されちゃってるはず)。nをブロック内で変更できて、スコープ外でも使えるブロックを返す関数を書くなら、

// C or ObjC
typedef int (^bint)(void);

bint make_incr() {
  __block int n = 0;
  bint f = ^int(void){ return n++; };
  return Block_copy(f);
}

こんな感じになる。ちなみに、このコードはObjCで書かれてる部分が無いのでMacOSX 10.6だったらCのコードとしてもObjCのコードとしてもコンパイルできるけど、ObjCの場合は<Foundation/Foundation.h>を、Cの場合は<Block.h>をimoport(include)する必要がある。あと、Block_copyして関数のスコープ外で使えるようにしたときは、当然呼び出し側で責任持ってBlock_releaseしてブロックを開放してやる必要がある。

// Cの場合
BLOCK_TYPE block = ^{ ... };
Block_copy(block);
...
Block_release(block);

// ObjCの場合
// Cと同じやりかたでも問題ないし、
// copyメソッドがBlock_copyと、releaseメソッドがBlock_releaseと対応してるので
// そっちも使える
BLOCK_TYPE block = [^{ ... } copy];
...
[block release];

// あとautoreleaseも使える
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
BLOCK_TYPE block = [[^{ ... } copy] autorelease];
...
[pool release]; // ここでblockも開放される

ちなみになんでNSMutableArrayの方は破壊的な操作をできるのかというと、多分変数の操作じゃなくてオブジェクトへのメッセージ送信をしてるだけだから。[newArray count]とかやるのと意味的には同じなのでできるってことじゃないのかな。で、newArray = ... とか変数自体をいじろうと怒られるはず。

次回予告

Grand Central Dispatchで遊んでみた編。多分。

faultierfaulist  at 21:00  | コメント( 2 )  | トラックバック( 1 )  | この記事をクリップ!

RubyエンジニアのためのObjective-C Blocks入門

書こう書こうと思ってたけど忘れてたのを、PerlエンジニアのためのObjective-C Blocks入門を見て思い出した。すいませんタイトルは便乗です。

試しに書いてみる

Blocksってのが何者なのかはさっきの記事なりAppleのドキュメントなりを見てもらえばいいと思うんですが、まぁウケが良さそうなので付けたタイトルにのっとってRubyと比較してみる。

f = lambda {|x|
  puts "#{x}のこと以外は何も考えられない"
}
f.call("うどん")
void (^f)(id) = ^(id x) {
  NSLog(@"世界の全てを敵に回しても、僕は%@の味方だ", x);
};
f(@"うどん");

なんだ、そっくりじゃない。似てる似てる。

上がRuby版、下がObjC版。下は普通引数にはNSString*とかを使うと思うけど、まぁRuby版と挙動を合わせるためにidにしてみた。それぞれコピペして動かしてみるといいよ。好きな子の名前とか入れてみるといいよ。

なに?ブロックを変数に入れてるとこの宣言がキモい?じゃあこうだ。こうすればいい。

id f = ^(id x) {
  NSLog(@"世界の全てを敵に回しても、僕は%@の味方だ", x);
};

id型の変数に代入できてしまった。実はこのブロックはオブジェクトなので、普通にid型として扱えるし、メッセージのレシーバになったりできる。もちろんid型の変数に入ってるときは関数みたいに使ったりできないので、実際使うときは

((void (^)(id))f)(@"うどん");

とかキャストしてやらなきゃいけないわけで、id型で宣言する意味はあんまりない。ただ、id型として扱えるってことは、NSArrayとかNSDictionaryにつっこんどいたりできるってことでもある。コレクションクラスとか自前で作らなくても他のオブジェクトと一緒にArrayにつっこんどいてうまいこと処理するとかできる。ところでObjC上ではObjCのオブジェクト扱いだけど、Cで使ってるときはこれ何として扱われてるんだろう。気になる。

せっかくなのでブロック構文

これだけだと何それおいしいので終わりなので、実際に使ってるところをみてみる。Rubyでblockって言ったらブロック構文を思い浮かべると思う。例えばこんなの。

languages = ['Ruby','Objective-C','Perl','PHP','JavaScript','Haskell']
languages.each do |l|
  puts "#{l}なら多分書ける"
end

Blocksと言うからにはこういう使い方をするメソッドがいくつかFoundationのクラスにもある。上記のコードをObjCでブロックを使って書くとこんなかんじになる。

NSArray *languages = [NSArray arrayWithObjects:@"Ruby",@"Objective-C",@"Perl",@"PHP",@"JavaScript",@"Haskell",nil];
[languages enumerateObjectsUsingBlock:^(id item, NSUInteger idx, BOOL *stop) {
  NSLog(@"%@なら多分書ける", item);
}];

う、うん、似てる…よね?ちょっとわかり辛いかもだけど enumerateObjectsUsingBlock:ってのが引数にブロックを取るNSArrayのインスタンスメソッドで、Array#eachと同じように自分自身の要素を一個ずつ順にブロックに渡して実行してくれる。ちなみに、「最初の引数がNSArrayの要素のオブジェクト、次がインデックスの整数値、最後がループを止めるためのBOOLのポインタ」という3引数のブロックを渡さないといけない。どっちかっつーとEnumerable#each_with_indexのが近いかな。

上の例ではitemしか使ってないけど、全部使うとしたらこんな感じ。3個目の引数にYESを入れてやるとそれ以降は実行されなくなる。

[languages enumerateObjectsUsingBlock:
  ^(id item, NSUInteger index, BOOL *stop) {
    NSString *status;
    if ([item isEqualToString:@"PHP"]) {
      status = @"アタシはしんだ。";
      *stop  = YES;
    } else {
      status = @"楽しかった。";
    }
    NSLog(@"%d日目。%@を書いた。%@", index+1, item, status);
  }
];

や、別にPHPについて何か言いたいわけじゃないですよ?やだなぁ、ちょっとした冗談じゃないですか。

今度書く

この記事の例では書いてないけど、所謂クロージャなのでブロックが生成されたコンテキストの変数とかブロックの中から参照できる。NSArrayにmapメソッド生やしてみたり簡易ベンチマーク関数作ってみたりしたので今度晒す。

あと、Enumeratorもいいけど、Blocksが真の力を発揮するのは、Cならdispatch_async、ObjCならNSOperationとかを使って非同期に処理をぽんぽん投げてくとき。というか、BlocksがそもそもGrand Central Dispatchという仕組みと一緒に導入されたものなので、それについてはもうちょい調べて今度書く。ちなみに例えばNSArrayでもenumerateObjectsUsingBlock:の他にenumerateObjectsWithOptions:usingBlock:というメソッドがあって、これにNSEnumerationConcurrentってオプションを渡してやると並列に実行してくれたりする。

余談

構文がキモいのはもうObjCだしどうしようもないけど、気に入らなければtypedefしておいた上でブロック生成のマクロでも書いとくと少しはマシに見えなくもないような気がしなくもないような感じがしたりしなかったり。うん。キモい。

余談2

gccだと独自拡張らしいので多分使えるのMacOSX 10.6のXcodeに付属してるやつだけだと思うけど、Clangには組み込まれてるらしい。試してないけど、Clangでなら他のプラットフォームでもBlocks使ったCのコードコンパイルできるんじゃないかな。

余談3

iPhoneSDKで使えるのかい?ってのは聞かないでくだしあ。泣きたい。

faultierfaulist  at 01:14  | コメント( 2 )  | トラックバック( 3 )  | この記事をクリップ!

NSURLConnectionあたりのテスト

お掃除

iPhoneアプリのコードのお掃除をしている。やれiPadだ新型iPhoneだOS4.0だ、うはぁ頼みの汎用ライブラリはUndocumentedなAPI使ってて動かない、なんですってー他の言語からのトランスレータは規約で禁止だってー、とまぁなんだかんだでiPhoneデベロッパはアプリの改修に追われててAppleに恨みつらみが募ってたりもするんじゃないかと思うけど、まぁ俺も大体そんな感じです。正確に言うとそうじゃなくてもしょっちゅう直してますけど。

リファクタリング

そんなわけで内部のコードの整理とかバグ取りとかついでに切り出したAtomPubクライアント汎用フレームワーク化しようかなーとかやってて、そうなるとちゃんとしたテスト書いてないと辛いのでOCUnitでテストケースをもりもり増やしてるんだけど、非同期にAPIと通信してるところがいまいちテストし辛い。というわけでNSURLConnectionを使ってるところを上手いことテストできないか試行錯誤してみた。

スタブに差し替えておく

まず実際にリクエストを投げないように、NSURLConnectionの実装を差し換える。実装の差し替えって言ったらposeAsClass:あたりかなーとか思ってたら、もうとっくにdeprecatedになってたのね。っていうかそういやそんな話聞いたの大分前だな。

そうするとRuntimeAPIのmethod_exchangeImplementationsあたりでメソッドを差し替えるのがいいのかなー。こんな感じか。

#import <objc/runtime.h>
#import <Foundation/Foundation.h>

void swizzle(Class target,SEL orig_sel,SEL alt_sel) {
    Method orig_method = class_getInstanceMethod(target,orig_sel);
    Method alt_method = class_getInstanceMethod(target,alt_sel);
    method_exchangeImplemantations(orig_method,alt_method);
}

これで例えば、

@interface NSURLConnection (ForTest)
- (void)_stab_start;
@end
@implementation NSURLConnection (ForTest)
- (void)_stab_start {
    // 元の実装だとこれを呼ぶと通信が始まるので、
    // 通信しないで直に
    // [self.delegate connectionDidFinishLoading:self]
    // とか
    // [self.delegate connection:self didFailWithError:error]
    // とかを送る処理を書く。
    // ただ非同期に処理が走(ってるように見せ)る必要があるので、
    // NSTimerとかを使って時間差で呼ぶようにしておく。
}
@end

みたいにしておいて、テストケースの方で

- (void)setUp {
    swizzle([NSURLConnection class], @selector(start), @selector(_stab_start));
}
- (void)tearDown {
    swizzle([NSURLConnection class], @selector(start), @selector(_stab_start));
}
- (void)testConnection {
    id hoge = [[Hoge allco] init]; // 内部でNSURLConnectionを使ってるクラス
    // 通信してますよー的な処理
    [[NARunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:5]];
}

とかやればいいはず。load時じゃなくてsetUpで差し替えてtearDonwで戻してるのは、他のテストにまで影響が出ないように。これでいいんだっけ。差し替えられたら以後そのセレクタになるはずだから戻すときは同じのをもう一度呼べばいいんだよね、多分。

○○はループ説(ひぐらし的な意味で)

ちなみに、テストケース中にそのまま非同期通信を含む処理を書いても上手く動かない。NSTimerとかNSThreadとかを使う処理はNSRunLoopが回ってるときじゃないと期待する動きをしない(例えばNSTimerは発火しないのでタイマーから呼ばれるはずの処理はそもそも通らない)んだけど、テストケースの実行はアプリケーションじゃないのでもちろんNSRunLoopは回ってない。NSURLConnectionの実装を差し替えたおかげで処理にどのくらいの時間がかかるか予想できるので、適当な時間を指定してrunUntilDate:とかで走らせてやればちゃんと動く。

あとは普通にテストケース増やしてけばビルドする度に網羅的にテストが走るので大分安心感が増す。しかしUIの方はどうしたもんかね。こればっかりは見ながらデバッグするしかないのかなー。世のデベロッパのみなさんはどうしてるんだろう。

faultierfaulist  at 01:28  | コメント( 0 )  | トラックバック( 0 )  | この記事をクリップ!

MacRubyさんにDTを喋らせたい

こんにちは、「それは一体誰得なんだ」でお馴染みのfaultierお兄さんだよ!今日はみんな大好きMacRubyをどれだけ無駄遣いできるかを考えてて例のごとく失敗したので、その顛末を教えてあげるよ!

MacRubyでDTを動かしたい

まぁ冒頭書いた通りなんだけど、「Objective-CからMacRubyを利用する - Watsonのメモ」を読んでなんか変なことできないかなーと考えてて、そういや俺ってば見た目に面白い以外は全く使い道のないものを以前作ってたじゃん、と思い出したんだけど、上手くいかなったという話。あ、全く使い道の無いものってのは、もちろん言うまでもなくあいつのことですね。

esotericは構成としては、ソースコードをパースしてSexpにするParserと、それをRuby2Rubyを使ってRubyのコードにトランスレートしてから実行するRunnerでできているので、MacRuby Frameworkを使ってesotericをObjCから呼び出せば、アプリケーションにDTやてってってーでプラグインを書ける仕組みを比較的容易に導入できるかと思います。導入したところで誰が使うのかわかりませんが。少なくとも俺は絶対に使わない。

まずは小手調べのコンパイルエラー

とりあえずMacRubyをDownloadしてくる。最新版の0.5はSnow Leopardにしかインストールできないけどこないだクリーンインストールしたばっかりだから全然問題ないもんね!と勝ち誇ってみせたけど、一体誰に勝ったのかはよくわからない。ちなみにソースからのビルドも時々試みてるけど大体こけるので今回は無難にバイナリをインストール。macgemは0.4のときはまともに使えたもんじゃなかったのでちょっと不安だったけど、Ruby2Rubyも特に問題なく入った様子。なに、こんな拍子抜けするくらいさらっと入っちゃっていいの?とニヤニヤしながら次のコマンドを実行。

$ pwd
/Users/taro/Projects/esoteric
$ echo $RUBYLIB
lib:
$ macruby -v 
MacRuby version 0.5 (ruby 1.9.0) [universal-darwin10.0, x86_64]
$ macruby bin/dt -v
/Users/taro/Projects/esoteric/lib/esoteric/dt/parser.rb:13: end pattern with unmatched parenthesis: /((?:\xE3\x81\xA9|\xE7\xAB\xA5\xE8\xB2\x9E\xE3\x81\xA1\xE3/
/Users/taro/Projects/esoteric/lib/esoteric/dt/parser.rb:74: end pattern with unmatched parenthesis: /(\xE3\x81\xA9|\xE7\xAB\xA5\xE8\xB2\x9E\xE3\x81\xA1/
dt.rb:3:in `<main>': compile error (SyntaxError)
    from dt:4:in `<main>'

オゥフ。言われたところを見てみたら、parser.rbの13行目には/((?:ど|童貞ちゃうわっ!)+)…/という正規表現が書いてあった。念のため試してみたけど、Ruby 1.9.1ではちゃんと動いてる。どうも、()の中にASCII以外の文字が含まれてるとMacRubyさんは閉括弧を見つけられなくて正規表現として不正だと言ってくる様子。ソースコードはutf-8で書いてあって、magic commentにもutf-8って指定してて、文字列リテラルだと問題ないのに、正規表現だと駄目。仕方ないのでベタに日本語書いてたところを全部Unicodeリテラルにしてみた。"ど"だったら"\u3069"とか。とりあえずそれでコンパイルできないというエラーは出なくなった。CRubyの方でももちろんちゃんと動く。なんだよ、やればできるんじゃないか、ツンデレか?などと思いつつhi.dtを実行させてみる。

$ macruby bin/dt -v
esoteric 0.0.2, DT 0.0.2
$ macruby bin/dt examples/hi.dt
parser.rb:160:in `numeric:': ArgumentError (ArgumentError)
from parser.rb:80:in `process'
from parser.rb:58:in `block'
from parser.rb:51:in `parse'
from parser.rb:11:in `parse:'
from runner.rb:25:in `run:'

ぬぅ。まだツンツンしてやがる。ちょっと勢い込んでしまったけど、どうもまだMacRubyと打ち解けきれてないみたい。ちなみに、CRubyの方でやるとこんな感じになる。

$ ruby -v
ruby 1.9.1p243 (2009-07-16 revision 24175) [i386-darwin10.0.0]
$ ruby bin/dt -v
esoteric 0.0.2, DT 0.0.2
$ ruby bin/dt examples/hi.dt
$stack = []
$heap = {  }
$stack.push(72)
$stdout.print($stack.pop.chr)
$stack.push(105)
$stdout.print($stack.pop.chr)
$stack.push(33)
$stdout.print($stack.pop.chr)
$stack.push(10)
$stdout.print($stack.pop.chr)
exit(0)
Hi!

うーん、ちゃんと動いてるよなぁ。該当の箇所を調べたら、本来encodingがUTF-8のStringが来てなきゃいけないところで、MacRubyの場合はUS-ASCIIなStringが来てしまっている。あるぇ?その文字列がどっから来てるかを辿って行くとARGF.readしてるとこなんだけど、MacRubyでは既にその時点でUS-ASCIIとして読み込んでしまっている。CRubyでやったらちゃんと動くのだから、$stdin.external_encodingはちゃんとUTF-8になるはずなんだけど、そもそもそこがnilだし、opneとかset_encodingとかで指定しても変化なし。force_encodingとかしても上手くいかない。と、このあたりでもっと色々なことがおかしいということに気付く。

MacRubyでStringが期待した挙動をしてない件

いまいち良くわからないので、試しにこんなことをしてみた。

$ cat test_string.rb
# coding: utf-8
a = "ど"
b = "\u3069"
puts "\"ど\".encoding        #=> #{a.encoding}"
puts "\"\\u3069\".encoding    #=> #{b.encoding}"
puts "\"ど\" == \"\\u3069\"     #=> #{a == b}"
puts "\"\\u3069\" == \"\\u0069\" #=> #{b == "\u0069"}"
puts "\"ど\" =~ /\\u3069/     #=> #{a =~ /\u3069/}"
puts "\"i\" =~ /\\u3069/      #=> #{"i" =~ /\u3069/}"
puts "\"i\" =~ /\\u0069/      #=> #{"i" =~ /\u0069/}"
$ ruby testb_string.rb
"ど".encoding        #=> UTF-8
"\u3069".encoding    #=> UTF-8
"ど" == "\u3069"     #=> true
"\u3069" == "\u0069" #=> false
"ど" =~ /\u3069/     #=> 0
"i" =~ /\u3069/      #=> 
"i" =~ /\u0069/      #=> 0
$ macruby test_string.rb
"ど".encoding        #=> UTF-16
"\u3069".encoding    #=> US-ASCII
"ど" == "\u3069"     #=> false
"\u3069" == "\u0069" #=> true
"ど" =~ /\u3069/     #=> 
"i" =~ /\u3069/      #=> 
"i" =~ /\u0069/      #=> 0

おぉう…どういうことなの…なんでこんなに違うの…。ちゃんとわかってないんだけど、こんな感じなのかしら。

  • MacRubyはソースコードがUTF-8で書かれているものと想定して、それをUTF-16に変換している?あと、magic commentを見てないようで、試しにeuc-jpで書いてみたらバイト列をそのままUTF-16の文字列だと解釈してStringクラスにしていて、化ける。
  • IOからの読み込みはASCIIとして扱っている。ARGFでもopenでも同じだった。こちらも環境変数、コードのencoding、magic comment、読まれるファイルのencodingに関わらず同じ。
  • String#encodeやString#force_encodingが何もしないでselfを返してるように見える。NSStringのメソッドを使って変換してやれば変わるんだろうか?
  • Unicodeリテラルを解釈するときに、\uXXXXの後ろ二桁しか見てないっぽい。"\u3069" == "\u0069"がtrueって何の冗談かと思った。
  • Unicodeリテラルの扱いが、文字列リテラルの中なのか正規表現リテラルの中なのかで違っている。"i" == "\u3069"はtrueだけど"i" =~ /\u3069/はfalse。そう言えば、"(ど)"は正しくパースできるのに/(ど)/はSyntaxErrorになるところを見ると、Unicodeリテラルに限らずそもそもそこのパースのロジックが微妙に統一されてない感じ。

さてどうしたもんかな…。日本語を正規表現でマッチさせてるところがまずい(ちなみにBrainf*ckは完全に、Whitespaceは不完全ではあるけど一応動いたので、ソースコードと入力にNON-ASCIIな文字列が含まれてなければ問題ないらしい)なら、完全にバイト列だと思って扱ってやるとか、正規表現じゃなくて==しか使わないとか(もちろんバイト単位で比較)、ObjCでまず入力を正規化してやった上でMacRubyに渡すとか(本末転倒!)、そういう風にすれば動かないでもないかもしんないけど、そういう文字列処理みたいなObjCであんまり書きたくないところをRubyでさらっと書けるから良いんであって、それ以外のところはそもそもObjCで書いたって大して難しくない。performSelectorとかランタイムAPIとか使いまくればいいんだよ!というわけでちょっと残念な感じ。

余談

esotericに付属のesocコマンドを使うとDTやBrainf*ckのコードをRubyのコードに変換できるので、出来たコードをmacrubycにかけてやれば最終的にMacOSXで動作するバイナリができます。DTのコードがなんと高速で動作するネイティブのバイナリに!…と思ったけど結構遅かった。なんかこう、色々読み込むののオーバーヘッドが馬鹿にならない感じ。でも、普通にRubyを使うとstack level too deepで動かないような深い再帰のコードでも動いたりする。ていうかexamples/fact.*、macrubycでコンパイルしないと動かないんですけど。何でこんなコード入れてんだ俺。

faultierfaulist  at 18:03  | コメント( 0 )  | トラックバック( 0 )  | この記事をクリップ!

livedoorブログにAtomPubで投稿する

もう、こなったら、iphone用livedoorアプリの出力するXML見て、自前でXML組み立てるかな。

誰か助けてください。(切実)

俺日記 : [HELP ME!]perlでlivedoorに投稿するときのカテゴリ設定方法

誰か助けてください、とのことなのでお助けします。…と、その前に一つ注釈というか弁明というかしておくと、実はそのAPI、AtomPubの仕様が完全に固まる前に実験的に作られたもので(まだAtomPPとか呼ばれてたはず)、色んなところがすごく古いです。

結果は惨敗。生成されたXMLを見てみると、ちゃんとcategory要素が追加されてるんだけどなー。

<?xml version="1.0" encoding="utf-8"?>

<entry xmlns="http://www.w3.org/2005/Atom">
  <title>ちょっとテスト(すぐ消します)</title>
  <content type="xhtml">
    <div xmlns="http://www.w3.org/1999/xhtml">いっぱいのつぶやき</div>

  </content>
  <category term="カテゴリ"/>
</entry>
俺日記 : [HELP ME!]perlでlivedoorに投稿するときのカテゴリ設定方法

AtomAPIの仕様を確認したら、atom:categoryじゃなくてdc:subjectを見ていた。どうも当時、2004年くらいはカテゴリーを表す仕様がなかったようで、dc:subjectで代用していたらしい。良く見たらXML::Atom::EntryのUsageにもそんな例が書いてある。ということで、dc:subjectのエレメントを作ってつっこんでやればカテゴリが追加されます。

です、が。AtomAPIはもともと正式にサポートしているものでないブログのQ&A:APIのURLは? - livedoor ヘルプ)上、見ての通り古いものなのでそのうちふさがれます。livedoor Blogで正式に公開してるAPIは今現在はAtomPubAPIの方なので、今AtomAPIでうまく動かないとか、今後Blogと連携するアプリを作りたいとかの場合は、こっちを使って下さい。ちなみにiPhoneアプリはこれを使ってる。

AtomPub APIの方を使って元記事のコードを書くとこんな感じ。あと、AtomPub APIは認証方式がWSSEなので、XML::Atom::ClientじゃなくてAtompub::Clientを使う。

#!/usr/bin/env perl

use strict;
use warnings;
use Atompub::Client;
use XML::Atom::Entry;

my $client = Atompub::Client->new;
$client->username('username');
$client->password('apikey');

my $service     = $client->getService("http://livedoor.blogcms.jp/atom/");
my $article_url = $service->workspace->collection->href;

# ブログが一個しかない場合は↑のコードでarticle collectionのURLが取れるけど、
# (workspaceの一個目がメインブログで、collectionの一個目がarticle collectionだから)
# ちゃんとやる場合は以下のように全部列挙したりして必要なのを選んでください。
# my @workspaces = $service->workspaces;
# for my $ws (@workspaces) {
#     my @collections = $ws->collections;
#     for my $c (@collections) {
#         print $c->title . "\n";
#         print $c->href . "\n";
#     }
# }

my $entry    = XML::Atom::Entry->new;
my $category = XML::Atom::Category->new;
$category->term('つぶやき');
$entry->title('これは、訓練ではない。');
$entry->content('繰り返す、これは訓練ではない。');
$entry->category($category);
$client->createEntry( $article_url, $entry );

あと他にもいくつか独自要素があるとか、実はlivedoor以外のBlogger Allianceのブログでも使えるとか、iPhoneアプリの絡みでときどきアップデートされるとか、あんま使い過ぎるとスパム判定されるのでほどほどにしてね、とかあるけど、その辺は追い追いどっかにちゃんと書きますんで、Wikiとか見てて貰えると。

faultierfaulist  at 20:42  | この記事をクリップ!

噛みまみた

最近ブログ更新してないなーと思ってふと最終更新日見たら3ヶ月も前の日付になっていた。良くない。ブログ書いてないということはブログに書けるような馬鹿なことをしてないということで、3ヶ月間一日も休まず馬鹿であったにもかかわらず馬鹿なことをしてないというのはとても良くない。これはアイデンティティの危機である。

ということで久々に馬鹿なことをしないとと思い立ったわけでは全然なくて、ただただ単純に八九寺が可愛過ぎるので、八九寺に名前を噛みまみたしてもらえるモジュールを作ってみた。結果的にだいぶ馬鹿になった。

faultier's kamimamize at master - GitHub

こんな風にして使います。

$ cat kamimamizer.rb
#!/usr/bin/env ruby
# coding: utf-8
require 'kamimamize'

mayoi = Kamimamize::Coverter.new({
    :pattern => { :priority => 10 },
    :repeat  => { :priority => 5, :through => 0.4 },
    :swap    => { :through => 0.05, :appid => 'APPID' },
    :default => { :class => 'Kamimamize::Plugin::Random' }
})

puts mayoi.kamimamize('阿良々木', 'あららぎ')
puts mayoi.kamimamize('羽川', 'はねかわ')

$ ./kamimamizer.rb
阿良々々々木さん
骨川さん (ほねかわさん)

名前とその読みを入れるとカミママイズされた名前が出てきます。カミママイズだなんていやらしいです。それだけです。それだけですが、何か?

一応ちょっとだけ解説すると、引数にプラグインの設定を入れてKamimamize::Converterをnewすると、kamimamizeするときにそのプラグインのどれかを使って名前を変換します。priorityが高い奴から順に処理されて、自分が処理できない名前のときは次のプラグインに任せます。throughが設定されてると大体それぐらいの割合で答えられるときでもスルーして次に渡します。上の例だとrepeatプラグインは4割くらいの確率で処理できるときでもスルーします。全部のプラグインがスルーすると最後にdefaultのプラグインが拾います。何も設定されてないとrandomプラグインがdefaultで、ランダムで八九寺にひどいことを言われます。

プラグインはとりあえず4つ作ってあって、

  • HashかYAMLファイルのパスで名前とそれに対応する変換結果のリストを渡すと、対応する名前が来たとき変換結果リストの中からランダムで返すpatternプラグイン。何もリストを渡さないと「八九寺」にだけ反応する。
  • 同じ文字が続く名前、もしくは「々」が含まれる名前のときにその数を増やしたり減らしたりして噛むrepeatプラグイン。
  • 名前の読みの母音や子音を適当に置き換えるswapプラグイン。Yahoo!のテキスト解析APIを使うのでアプリケーションIDが必要。ちなみに、名前の字面を見ているわけではなく読みを適当に置き換えるだけなので、そんなに上手いことは言わない。
  • 返答リストからランダムに返すだけのrandomプラグイン。返答リストは初期化時に設定できるけど、しないと八九寺にひどいことを言われる。Converterをnewするときに明示的にdefaultを設定しない場合、このプラグインがthrough=0で一番後ろに設定される(つまり上の例ではdefaultの設定は意味ない)。

がある。上の例だと阿良々々々木さんはrepeatプラグインで、骨川さんはswapプラグインの変換結果。何かと適当なので多分そんなに面白い結果は出ないと思うけど(例えば戦場が原とか入れると意味不明すぎて噛んだのかどうかすら怪しいのが出てくるとか、阿良々木さんをカミママイズすると木が消えちゃうとか)、個人的には羽川が骨川とか花沢に変換されたのが面白かったので満足です。ひらがなの音を置き換えるのって結構面倒なのよ。というか八九寺の神懸ったセンスに勝てる気がしない。

あとは類義語に置き換えるとか、名前の中の語彙の分割を真面目にやるとか、specくらい書くとかしたいけど、多分飽きてやんないだろう気配がします。自分で言うな。なんというか、カミママイズするためだけにY!のAPIを使うという無駄遣いをしたかっただけだったりする。Y!のAPI面白かった。途中で形態素解析とかして遊んでた。どうせならもっと盛大に無駄遣いしてみたいなあ。

ねんどろいどぷち 化物語セット 其ノ貮 (ノンスケールABS&amp;PVC塗装済み可動フィギュア)
ねんどろいどぷち 化物語セット 其ノ貮 (ノンスケールABS&amp;PVC塗装済み可動フィギュア)
クチコミを見る
ねんどろいどぷち 化物語セット 其ノ壹 (ノンスケールABS&amp;PVC塗装済み可動フィギュア)
ねんどろいどぷち 化物語セット 其ノ壹 (ノンスケールABS&amp;PVC塗装済み可動フィギュア)
クチコミを見る

余談

上のねんぷちの化物語セット、これを書いてる時点ではAmazonでは受付終了しちゃってるんですが、それを今更気付いてものすごく落ち込んでたら、twitterで

と八九寺が教えてくれるという素敵展開が。@mayoi_hachikujiには足を向けて寝れそうにないと思いながらどこにいるか知らないので普通に寝ました。ありがとう八九寺。大好きだ八九寺。

faultierfaulist  at 17:36  | コメント( 0 )  | トラックバック( 0 )  | この記事をクリップ!
livedoor プロフィール
記事検索
月別
カテゴリ別