As Sloth As Possible

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

カテゴリ: プログラミング

友達と遊びに行くつもりでいたらなんかみんな都合合わないとかでお流れになりそうな雰囲気なので、せっかくだから久々にMerbでもいじってみることにした。ん?コミケ?行ってないよ。月曜に有給取ったら職場の人に「週末は戦場に行くので週明けに戦士の休息ですね、わかります」的なリアクションをされたんだけど、いや、別にそういうわけじゃ…。わかんないけど。明日暇に耐え兼ねて行くかもしんないけどさっ!うう…。

思い付きでupdateするな俺のバカ

とりあえず以前の記事で軽く動かしてみる程度はやったものの、MerbもDataMapperもしばらく見てない間に更新されてて、深く考えずにgem updateしたらハマった。Merb本体のバージョンが0.9.4に上がってて、Merbをアップデートしたらextlibのバージョンも0.9.4になっちゃったんだけど、DataMapperのバージョンは0.9.3までしか上がってない。gemのdatamapperが依存してるdata_objectsがextlibの0.9.3を要求する(dependencyが'= databjectsのバージョン'になってる)ため、 「extlibを一旦0.9.3にしてからdata_objects、datamapper、merb_datamapperをアップデート」してみた。

入るには入るんだけど、もちろんextlibが0.9.3のままだと最新のMerbが動かないし、extlibを0.9.4にするとこんどはdatamapperがrequireできない。うがー。ActiveRecordやSequelを使うのも考えたけど、そもそもARを使いたくないからDataMapperを試しているのでARは無いし、Sequelも面白そうなんだけど、Merbのプラグイン?で使ってみたいのがARかDataMapperを想定してるのでSequelは使えない。ので却下。ソースをいじるとかも考えたけど、無意味に面倒な上危ないので却下。どうせリスクがあるんならedge版入れちゃえ、そもそも実験だし、てことでgithubからDataMapperの最新版取ってきて入れた。手順はMerb wikiを見ながらやってみた。

$sake -i http://github.com/dkubb/dm-dev/tree/master/dm-dev.sake?raw=true
$ sake dm:clone
$ cd dm
$ sake dm:install

DataMapperの0.9.4があっさり入りやがった。ぬーん。とりあえず今のところ問題なく動いてる様子。DataMapperがMerb本体に深く影響されるのはなんだか納得行かないんだけど、ARだってRailsにべったりだしなぁと考え直すことにする。

なにはともあれdatabase.yml

とりあえずアプリの雛形を作る。

$merb-gen app --orm datamapper sample

ARが本体の一部であるRailsと違って、O/Rマッパーが本体と分離してるMerbはこの時点ではdatabase.ymlとかmodelsディレクトリとかは作られない。

$rake -T | grep dm
rake dm:db:automigrate            # Perform automigration
rake dm:db:autoupgrade            # Perform non destructive automigration
rake dm:db:create                 # Create the database (postgres only)
rake dm:db:database_yaml          # Create a sample database.yml file
rake dm:db:drop                   # Drop the database (postgres only)
rake dm:db:migrate                # Migrate the database to the latest version
rake dm:db:migrate:down[version]  # Migrate down using migrations
rake dm:db:migrate:up[version]    # Migrate up using migrations
rake dm:db:reset                  # Drop the database, and migrate from scr...
rake dm:sessions:clear            # Clears sessions
rake dm:sessions:create           # Perform automigration for sessions
$rake dm:db:database_yaml

これでconfig/database.yml.sampleが出来るので、それをコピーして弄る。中身はRailsでアプリ作ったことある人なら見慣れてるであろうデータベースの設定なので、適当にいじる。

# config/database.yml
---
development: &default
  adapter:  mysql
  encoding: utf8
  database: sample_development
  username: user
  password: password
  host:     localhost

そうしたら、

$merb-gen model user
Generating with model generator:
     [ADDED]  spec/models/user_spec.rb
     [ADDED]  app/models/user.rb

しておいて、app/models/user.rbを、

# app/models/user.rb
class User
  include DataMapper::Resource
  property :id, Integer, serial => true
  property :name, String, nullable => false
end

のようにいじった上で、rake dm:db:automigrateする。これで、DBを適切に設定してあればidとnameというカラムを持ったusersテーブルが出来てるはず。とりあえずここまでできたらmerb -i(Railsで言うところscript/console)とかやっていろいろ遊んでみるといい。

repositoriesってなんぞ

いじってる途中で気付いたんだけど、database.yml.sampleの中にこのような記述が。

development: &defaults
  # These are the settings for repository :default
  adapter:  postgres
  database: sample_development
  username: the_user
  password: secrets
  host:     localhost

  # Add more repositories
  # repositories:
  #   repo1:
  #     adapter:  postgresql
  #     database: sample_development
  #     username: the_user
  #     password: secrets
  #     host:     localhost
  #   repo2:
  #     ...

調べたら、repositories以下に上のと同じ形式でdbの設定を書いて置くとそれを使って接続できる様子。ふむふむ。それなら、とさっきのdatabase.ymlをちょこっと書き換えてみる。

# config/database.yml
---
development: &default
  adapter:  mysql
  encoding: utf8
  database: sample_development
  username: user
  password: password
  host:     localhost

  repositories:
    slave:
      adapter:  mysql
      encoding: utf8
      database: sample_development_slave
      username: user
      password: password
      host:     localhost

これでとりあえずOK。slave、にしたのはdefaultの設定のDBからレプリケーションしてるスレーブのサーバを想定してるんだけど、そもそも全然用途の違う別のDBでもいい。で、使うときはこんなかんじ。

# マスターのサーバから取ってくる
user = User.first
# こっちはスレーブから取ってくる
user_s = User.first(:repository => repository(:slave))

おお。簡単だ。もうちょい説明すると、allとかfirstとかのfindしてくる系統のメソッドに、{:repository => DataMapper::Repositoryのオブジェクト}って引数を渡してやると、そのDB設定で探してくる。database.ymlの設定からDataMapper::Repositoryのオブジェクトを取得するには、repository(設定名)というメソッドを使う。

毎回これをやると面倒なので、参照用のときは必ず:slaveから取ってくるようなメソッドをUserに生やしておくといいかもしれない。逆でもいいな。更新が必要なときだけマスターを見るようにしとくとか。こっちの方が安心か。おそらくモデル単位でどこのリポジトリを使うかを設定できるはずなので、普通のデータは:defaultのDBに入れておいて、あるモデルは:portalに入れておく、とかもできる。DataMapper::Repositoryは動的に生成できるので、分散もそんなに難しくなさそうだ。Rails+ARだと多分プラグインを使わないとできなかったと思うけど、標準でこの機能が付いてるMerb+DataMapperはなかなか素敵。…いや、そう聞いてたからDataMapperを試してみたくなったんだけども。最初からこれが言いたかっただけだったりする。

次はmerb_openidでも

ユーザ認証にOpenIDを使おうと思ってruby-openidを使ってあれこれ試行錯誤してる最中に、merb_openidというgemがあることに気付いた。これを使えば簡単にOpenID認証するコントローラが書けそう。明日あたり試してみよう。

ブログネタ
Objective-C に参加中!

詳解Objective-C 2.0読書会に参加してきた。読書会というのに参加したのは初めてだけど、皆で同じ本を読んで、読んだ部分に対して「ここがわからない」「いやそれは実はこういうことで…」というやりとりは中々面白かった。一人で読んでるとスルーしがちなところも拾ってみると勉強になるなぁ。

それより何より、Objective-Cなんていうマニアックな言語の、読書会なんていうストイックな催しに、24人も集まったのが凄い。これもiPhone効果かーなんて思ったけど、現時点ではiPhoneを買うと表明した人が少なかったのも印象深かった。いや俺も買わないけど。とは言えなんだかんだで衝動買いしかねないけど。どっちだよ。

今日はCHAPTER2とCHAPTER3の途中までを読んだ。CHAPTER2はObjective-Cの基本的な特徴や構文、CHAPTER3がクラス定義と継承だった。内容に関しては詳解Objective-C 2.0を読んでもらうとして、以下は読書会中に挙がった話の補足。

[[Hoge alloc] init]について

ObjCでクラスからオブジェクトを作る場合のイディオムについての話。「allocの後に続けて初期化処理を書かないといけないと言われたことがあるが、それは何故か、どう危険なのか、またそれが必須ならなぜ一纏めにしないのか」との質問が挙がった。

allocの後にinitもしくはinit...で始まるメッセージを続ける理由は、呼び出し側でそれを分けて使うことがまず無いから。別に、

id obj = [Hoge alloc];
/* なんか別な処理 */
[obj init];

としたところで文法上は問題無いんだけど、NSObjectのクラスメソッドであるところのallocは単にメモリを割り当ててオブジェクトを生成しただけで、それがオブジェクトとして振る舞うためには最初にNSObjectのインスタンスメソッドのinitが呼び出されていなければならず、上のコードの変数objは「なんか別な処理」をしている最中には、「何もできないか、何かさせようとすると異常な挙動をする」。詳しくは見てないけど、NSObjectのinitは結構色々やってるはず。そんな不安定な状態のものを放っておくメリットはよっぽど特殊なケースでないと皆無なので、allocと初期化処理は同時にやってしまうようにしましょうと言う話。

じゃあ何でメモリ割り当てと初期化が別なんだ、一緒でいいだろ、と言われるとちょっと勉強不足で即答できないんだけど、内部的には未初期化のオブジェクトが効果的に使われてるところがあるのかもしれない。そういう設計思想だからじゃないか、としかわかんないなー。

ちなみに、「allocしてinitしたオブジェクトを返すクラスメソッド」はある。NSObjectにnewってメソッドが定義されてるので、上記のイディオムの代わりに、

id obj = [Hoge new];

って書いても同じ意味になる。ただこれだと引数なしのinitを呼び出すだけなので、引数付きで同等のことをやりたかったらそういうことをするクラスメソッドを定義してやる必要がある。あと、Cocoaのクラスでも初期化で色々ややこしいことしてるやつは、そういうクラスメソッドが用意されている。例えばNSArrayのarrayWithObjects:とか。ちなみに、Fooクラスのクラスメソッドの「fooWithHoge:」と、Fooクラスのインスタンスメソッドの「initWithHoge:」は対応させるのが慣習になっているようだ。

初期化処理のイディオム

詳解Objective-C 2.0には、初期化処理は通常次のようにする、と書かれている。

- (id)init
{
    self = [super init];
    if (self != nil)
    {
        /* 何らかの初期化処理 */
    }
    return self;
}

これは実は、次のように書いても大抵の場合問題ない。

- (id)init
{
    [super init];
    /* 何らかの初期化処理 */
    return self;
}

スーパークラスの初期化処理を明示的に呼び出すことと、returnを省略できないのは変わらないんだけど、明示的なselfへの代入とnilチェックはしなくても処理できる。スーパークラスの初期化処理を辿ってNSObjectのinitが呼び出されると自動的にselfへオブジェクトが代入されるし、仮にselfがnilでも「nilに対するメッセージ送信は単に無視される」。まぁインスタンス変数の扱いとかは危ういけど。

じゃあ何でわざわざselfへの代入を明示的にするのかと言えば、「selfに自分自身じゃないオブジェクトを代入してもいい」上に「スーパークラスのinitの返り値が自分自身とは限らない」から。そういうトリッキーな実装も可能なので(例えばNSStringとかは実は結構トリッキーなことをやってる。その話が出てくるのは大分先だけど)、何か特殊なことをするのでない時は上のイディオムで書いた方がいい。てかselfが自分自身じゃなくてもいいって凄いな。お前は誰だ。

superの意味

selfが単にオブジェクトだったのに対し、superは変数に代入したり付け替えたりはできない。多分予約語なんだよな。superは何かと言うと、「自分より上位の継承関係を辿って行って最初に見つけたクラスの実装を使って、自分のコンテキストでその処理を実行する」という構文。難しいね。

基本的には一つ上のスーパークラスを見て、なければさらに上へ…を繰り返して最終的にはNSObjectまで行く。ここで「一つ上からじゃなくて、スキップしていくつか上のクラスのメソッドを呼び出せるか」という話で盛り上がった。聞いて驚いたんだけど、実はすごくごちゃくごちゃしたコードを書けばできるらしい。けど、実行時に既存のクラスの書き換えまでやってのけるObjective-Cにおいて、コード上の継承関係を元にややこしい処理を行なうのは危険すぎる。最終的には「できるかできないかより、そんなことをしなきゃならないような設計の方が問題だよね」というオチがついた。そりゃそうだ。

メッセージセレクタ

「メソッドを呼び出す際に引数をつけるときには最後に:を付ける、複数の引数があるときは"keyword:"を並べる」というのを読んで、「第二引数以降にキーワードを付けるのは分かったけど、第一引数の前にはメソッド名があるだけでキーワードが無いけど、付ける方法はないのか」というの質問が上がった。ここはちょっと誤解しやすいポイントかも。多分、メソッド名とかメッセージキーワードとかいう語彙が悪い。

例えば、

[obj setValue:@"hoge" forKey:@"name"];

こういうときの「メッセージセレクタ」は「setValue:forKey:」のひとまとまりであって、その際に:や後ろのキーワードも含む。ちょっと無理矢理にRuby風に書くと、以下のようなイメージ。

# Rubyではメソッド名に:は使えないけど、仮に使えるとしたら
obj.setValue:forKey:("hoge", "name")
# もっと言うと、こういうイメージかも
# この方が「メッセージパッシング」っぽいし、
# 実際こういうメソッド呼び出しの方法はObjCにもある、
# というか内部的にはCの関数でこういうことをしている
obj.__send__(:setValue:forKey:, "hoge", "name")

なので、「最初の一個目がメソッド名で、二個目以降がキーワード」というのは誤り。一見分かれてるようだけど「全部くっつけたもの = メソッド名」だと思った方がいいし、セレクタ(SEL)を使って上のRubyの__send__みたいなことをやるときは正に、

SEL sel = @selector(setValue:forKey:);

などとして「メソッド名を生成」する。

ちなみに、最初の「一個目の引数の説明はどこに書くのか」に対しては、「何の引数を取るのかわかるメソッド名にする」が一応答えかな。Cocoaだと一個の引数を取るメソッドは例えば、

id url = [NSURL URLWithString:@"http://www.apple.com"];

のように、何の引数を取るか分かるように命名するのが慣習になっている。さっきのsetValue:forKey:も、「最初が値になるオブジェクトで、次がキーになるオブジェクト」ってのが分かるようになってるね。

ところで、ちょっと違う話になるけど、

# これはさっきの偽Ruby
obj.setValue:forKey:("hoge", "name")

これ何か見覚えあるなぁと思ったら、

# これはRubyCocoa
obj.setValue_forKey("hoge", "name")

RubyCocoaでOSX::NSObjectを継承したクラスのメソッドを呼び出すときとほぼ一緒(まぁ、そういう風に作ってあるのだけども)。RubyCocoaからMacアプリの世界に入った人は案外、ObjCのメソッド送信がすんなり分かるかもしれない。でもそれ以前にCocoaを知らないとRubyCocoaは分かりづらいって話はあるけど。ジレンマ。

詳解Objective-C 2.0読書会の今後

今日はとりあえず「音読して、途中途中で質疑応答タイムを設ける」っていう形式だったけど、今後は色々試行錯誤していくとのこと。あと、今回は休日だったけど、2時間程度であれば別に平日でも良いのでは?との意見から次回は試しに平日にしてみることになった。多分今月中にもう一回ぐらい開催されることになりそうなので、興味ある人はメーリングリストに参加してみるといいよ!

iPhone 用の開発は Objective-C なのか。これまたマイナーな……。真っ先に思い浮かんだのが io だった自分もちょっとどうかと思うんだけどね。しかし最悪でも Java にしようとは思わなかったのかな。
もしかして全然興味が無いObjective-Cを覚えなきゃならないのかなあ。

思ったか思わなかったかはわからないけども、正直その選択肢は無かったか有ってもかなり可能性は低かったはず。Javaが良い悪いではなくて、iPhoneがMac OSXである以上、Javaをメイン開発言語として採用するのは変だから。

だって、Mac OSX自体がそもそももうCocoa-Javaのサポートをしてないから。Macのネイティブアプリ開発環境はObjCとCに完全にシフトしてるし、他言語とのブリッジはRubyCocoaやPyObjCなどLLの方面に力を入れている。つまり既に一度「ObjCなんてマイナーな言語はみんな使ってくれないから、Javaで書いてもらおうぜ → やっぱMac OSXはObjCだよObjC」っていう道を辿ってるんだよね、Appleは。

仮にJavaをiPhoneの開発言語にするとしたら、一度は捨てたCocoa-Javaを無理矢理掘り起こしてくるか新しく作り直すかして、ただでさえリソースがタイトなiPhoneにJavaVM+iPhoneUI用のクラスライブラリを載せなきゃならなくて、そのコストを支払ってまでJavaにするメリットがあるかと言うと微妙。「Java開発者がiPhoneアプリを作り易い」よりも、「iPhoneのアプリを作りたいからObjCを勉強する」って人が増えてくれる効果の方がAppleにとっては嬉しいはず。というか、それだったらどうせならRubyCocoaやPyObjCを載せるんじゃないかなーと思う。

何より、マイナーマイナーとは言うけども、現実に「Mac OSXはObjCをメインの言語として採用」してるわけで、Macアプリ開発者は数はそう多くないけどそれなりにはいる。少なくとも今やObjCはもう「誰も知らないし、誰も使ってない」言語じゃない。それにObjCはあれで結構使い勝手が良いし、Cocoaもかなり強力だよ。あのダイナミックさ加減とか、Smalltalkっぽい文法とか、慣れると面白くなってくる。要するにObjC可愛いよObjC。

まとめると、

  • JavaはMacOSXにとってはそんなに重要な位置を占めてない
  • リソースのタイトなiPhoneにJavaVM+Cocoa touch互換クラスライブラリを載せる余裕は無さげ
  • というかObjC可愛いよObjC

ってことで全然興味が無いとか言わずに「マイナーな」Objective-Cでもいじってみてはどうでしょうか。

LDR&Fastladderの中の人から「これ面白そうだから、OpenFLで使ってみようよ。ていうか作ってよ」と大変無茶振りをされたのでYet Another PraggerでFastladderのクローラを書いてみた。

YapraかわいいよYapra

使ってみた感想としては良い感じ。Class-basedなプラグインなところが本家PRaggerより好みだし、でもlegacyなプラグインも読み込める互換性が使い勝手がいい。あと「実行時に使うプラグインだけ読み込む」ってことをしてくれるのも素敵。

肝心のFastladderのクローラは

iDiskに置いといたので興味のある方は持ってってください。展開したら中身をlib-plugin/yapra/plugin以下あたりに配置すると使えると思います。YapraはYuanyingさんのところから。ちなみにconfig.yamlのサンプルはこんな感じ。

- module: Fastladder::Config
  config:
    root: /path/to/fastladder/root
    environment: development
- module: Fastladder::CrawlTarget
- module: Fastladder::Fetch
- module: Fastladder::Store

最初のConfigでfastladderのパスとRAILS_ENVを指定して初期化して、CrawlTargetでDBからFetch対象のフィードを選んで、Fetchでフィードを取得、StoreでDB内のデータを更新、という流れでございます。んで、なんでわざわざPragger(Yapra)で作ってみたかというと、プラグイン形式のものにクローラを置き換えることを検討しているので。

例えば今のクローラは単順に古いのから順にフィードの取得をしに行くんだけど、もっと賢く(更新頻度が高いフィードや購読者が多いフィードを優先してフェッチする、とか)したければCrawlTargetだけ別なプラグインと置き替えてやればいい。例えばフィードを全文表示に展開するとか広告を除去するとか特定の記事だけを抽出するとかそういうフィルタをかけたければ、FetchとStoreの間にフィルタプラグインを挟んでやればいい。とか、そういうことがやりやすくなるかなと。

まぁPlaggerで既にやってる人は多々いると思うんだけど、正直Plaggerはインストールの敷居が高いし、同じRubyで書ける方がとっつきやすい人もいるだろうなと思うので、もしプラグイン風のクローラを採用するとしたらPraggerかYapraかなーという感じかなぁと話してるところです。Praggerくらいのサイズなら同梱できちゃいそうだし、なんならそれっぽい機構のものを再発明するのもありかも。

まあ一応念の為

これはあくまで「試しに作ってみた」だけのものなので。「添付のクローラより格段にすごい!」とか「PlaggerのStore::Fastladderなんか目じゃないぜ」みたいなものではありません。正直中でやってることは添付のクローラをほぼそのまま移植しただけなので、あちこちアレなところも直してないですし、fastladder本体にいろいろ依存してるので大変気持ちの悪い箇所が多々あります。今のところ既存のPraggerプラグインとの連携もできてないし、便利プラグインも作ってないです。あくまで試作品ですのでご了承下さい。

てか誰か作ってくれないかな。よさげなもの。上の試作品を完成させてくれちゃっても良いよ!(他力本願)

追記

流石にiDiskに置いとくのはアレなので、とりあえずgithubに上げときました。ついでにまた少しいじった。

昨夜詳解 Objective-C 2.0が届いてたので、週末はObjective-C漬けになろうと思ってたんだけど、何故かMerbを弄るのにはまってしまった。ということで遅蒔きながらMerb+DataMapperをいろいろ調べた。

ちょっと触ってみた感触としては、良いね。基本はRails+ARで書くのとそんなに変わらない。良くわかんないけど勘で「Railsだったらこうだよな」で書いても大概のものは動いた。ふむ。あとはパフォーマンスがどうなるかだけど、こればっかりはまともなアプリ書いてみないとわかんないな。前に頼まれて作ったとあるイベントの管理システムを試しにMerb化してみるか。FastladderをMerbに移植してみるとか(ry。いいからバグ修正しろって怒られそうなのでやめとこう。ていうか忙しくて止まってたもろもろの修正はやくコミットしないと。

DataMapperは結構気にいった。モデルがそのままスキーマの定義になるのはわかりやすいなぁ。複数DBも簡単に使えそうな印象だけど、Merbでどうやるのかちょっとわかんなかった。調べてみるか。

それにしても、サイトのサンプルコードと最新版の仕様が全然違うのにはちょっと泣けた。Getting Startedに書いてある通りにやろうとしたら、まずインストールするgemの名前からして違うとか!サンプルに書いてあるモジュールも存在すらしないし!えー。日本語の最新の情報が楽に手に入るようになるまでは時間かかりそうだし、APIリファレンスとソース読みつつ試行錯誤するしか無いか。

一応試しに書いてみたコードを曝しておこう。

require 'data_mapper'

DataMapper::Database.setup({
  :adapter => 'sqlite3',
  :database => 'dm_test'
})

DataMapper::Database.setup(:second_database, {
  :adapter => 'sqlite3',
  :database => 'dm_test2'
})

class Rabbit < DataMapper::Base
  property :name,   :string
  property :color,  :string

  # pの出力が見にくかったので
  def inspect
    "<#{self.class}:#{self.id},#{self.name},#{self.color}>"
  end
end

Rabbit.auto_migrate!

rabbit = Rabbit.new
rabbit.name = 'Jitterbug'
rabbit.color = 'orange'
rabbit.save!

Rabbit.create({:name => 'Mokha', :color => 'chestnut'})

puts "default database"
p Rabbit.all
p Rabbit.first(:name => 'Mokha')

puts "-----"

DataMapper.database(:second_database) do
  Rabbit.auto_migrate!
  Rabbit.create({:name => 'Tango', :color => 'white'})
  puts "second database"
  p Rabbit.all
end
$ ls
rabbit.rb
$ ruby rabbit.rb 
default database
[<Rabbit:1,Jitterbug,orange>, <Rabbit:2,Mokha,chestnut>]
<Rabbit:2,Mokha,chestnut>
-----
second database
[<Rabbit:1,Tango,white>]
$ ls
dm_test dm_test2 rabbit.rb

ちなみにコード中のはうちのうさぎ達の名前。一匹だけダンスつながりじゃないのは俺が名付け親じゃないから。


ジルバの写真で和むがいい。

ブログネタ
Objective-C に参加中!

tomyheroさんに書いて書いてとせがまれたので、セクシーObjective-Cブログに記事を書いてみることにした。Objective-Cの情報ってWeb上でもあんまり無いから、こういうのって素敵だ。頑張ろう。

とりあえずXcodeは一切使わないでObjective-Cの基礎っぽい内容を書いてこうかなと思ってるところ。ちなみにセクシーObjective-CブログはOpenIDでログインして記事書けるから、Objective-Cを使ってる人はどんどん書いてくれればいいと思うよ。

あと、なんでsexyなのかはさっぱりわからん。きっとtomyheroさんの趣味に違いない。

昨日の夜からハチロク世代の開発合宿に参加している。千葉の笹川で二十余名、いつものメンバーもいれば始めましての人もいて、上野氏がいたりamachangさんやnishioさんがいたりと面白い面子。

遠いわ!

まず、宿泊地の笹川、遠いよ。仕事が終ってから行ったので一人で夜に向かったんだけど、赤坂から軽く2、3時間、目的地が近付くにつれ周囲に街灯もなくなってきて非常に心細かった。着いたら何にもないし、怖いので駅からそう遠くないと聞いてたけどタクシーで旅館まで。タクシーの運ちゃんに「ハチロク世代の人?」と聞かれて、なんだなんだハチロクはこんなとこまで名が知れわたるようになったか、と一瞬思ったんだけど、旅館の前に「ハチロク世代開発合宿様」と書かれていて思わず笑ってしまった。そりゃ、何の集団かと思うよな。

御飯旨い

御飯にボリュームがあるところは素敵です。飯時でもプログラミングの話か上野氏の話しかしてないとか、変態集団だな。

「あのテンプレートのせいで、私の印象が一人歩きしている」(上野氏)

そんなこと言うからまたネタにされるという。彼は面白いなぁ。

開発中

とりあえず合宿中にCocoaでPICSのAPIを叩くフレームワークと、できればそれを使ったデモアプリくらいは作ろうと思ってるんだけど、これは順調に進んでいる。Objective-Cを使う奇特な人はやっぱり俺だけっぽいけど、CとかAtomAPIとかで困っても周囲にいくらでも聞ける人がいるってのは心強いな。開発合宿なるものに参加するのは始めてだけど、これは良い。

あとHashがRubyCocoaをやると言うので、隣の席陣取ってちょくちょくお節介してる。ちょっかい出したくてうずうずしてるのでハマったらもっと声掛けてくれるといいよ!

この間のiPhoto Exporter作りの続き。

先週の時点でX-WSSEヘッダ付けてAtomAPIのエンドポイントGETしてレスポンスからNSXMLDocumentを作る、ってとこまではやったのだけど、どうもWSSE認証に失敗してるらしくLogin Invalidとしか返ってこなかった。Rubyでだったらサクっと書けたんだけど、CocoaでSHA1とかBase64とか結構面倒臭い。むぅ。

で、その辺で挫けかけたので一週間ほど放置してたんだけど、ググったらopensslのライブラリでやればいいじゃんってことがわかった。こんな感じでNSStringとNSDataにメソッドを生やしてみた。あとはXcodeのビルドオプションで「その他のリンカフラグ」に「-lcrypto」と追加してコンパイルしてやるだけ。

// Crypto.h
#import <openssl/md5.h>
#import <openssl/sha.h>
#import <openssl/evp.h>
#import <openssl/bio.h>
#import <openssl/buffer.h>

#import <Foundation/Foundation.h>

@interface NSString (Crypto)

- (NSData *)dataHashedWithSHA1;
- (NSString *)stringHexHashedWithSHA1;
- (NSString *)stringEncodedWithBase64;

@end

@interface NSData (Crypto)

+ (NSData *)dataWithBase64String:(NSString *)pstrBase64;
- (NSData *)dataHashedWithSHA1;
- (NSString *)stringHexHashedWithSHA1;
- (NSString *)stringEncodedWithBase64;

@end
// Crypto.m
#import "Crypto.h"

@implementation NSString (Crypto)

- (NSData *)dataHashedWithSHA1
{
	return [[self dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:NO] dataHashedWithSHA1];
}

- (NSString *)stringHexHashedWithSHA1
{
	return [[self dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:NO] stringHexHashedWithSHA1];
}

- (NSString *)stringEncodedWithBase64
{
	return [[self dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:NO] stringEncodedWithBase64];
}

@end

@implementation NSData (Crypto)

- (NSString *)stringHexHashedWithSHA1
{
	unsigned char digest[20];
	char finaldigest[40];
	int i;
	
	SHA1([self bytes],[self length],digest);
	for(i=0;i<20;i++) sprintf(finaldigest+i*2,"%02x",digest[i]);
	
	return [NSString stringWithCString:finaldigest length:40];
}

- (NSData *)dataHashedWithSHA1
{
	unsigned char digest[20];
	
	SHA1([self bytes],[self length],digest);
	
	return [NSData dataWithBytes:&digest length:20];
}

+ (NSData *)dataWithBase64String:(NSString *)aString
{
	BIO *b64, *bmem;
	int length = [aString lengthOfBytesUsingEncoding:NSASCIIStringEncoding];

	char *buffer = (char *)malloc(length);
	memset(buffer, 0, length);
	b64 = BIO_new(BIO_f_base64());
	bmem = BIO_new_mem_buf((char *)[aString cStringUsingEncoding:NSASCIIStringEncoding], length);
	bmem = BIO_push(b64, bmem);

	BIO_read(bmem, buffer, length);
	BIO_free_all(bmem);
	
	return [NSData dataWithBytes:buffer length:length];
}

- (NSString *)stringEncodedWithBase64
{
	BIO *bmem, *b64;
	BUF_MEM *bptr;
	
	b64 = BIO_new(BIO_f_base64());
	bmem = BIO_new(BIO_s_mem());
	b64 = BIO_push(b64, bmem);
	BIO_write(b64, [self bytes], [self length]);
	BIO_flush(b64);
	BIO_get_mem_ptr(b64, &bptr);
		
	NSString *buff = [NSString stringWithCString:bptr->data length:bptr->length-1];
	BIO_free_all(b64);
	
	return buff;
}

@end

Objective-Cなので見慣れない[]とか@とか入ってるけど、中身をよくよく見るとどう見てもCです。本当に(ry。Cのコードやライブラリをそのまま転用できるのは素敵だなぁ。Objective-Cで便利なフレームワークとか見つけられなくてもCで探せば大概ある。こりゃいいや。

APIを叩く準備が出来たので、早速iPhoto Exporterに取り掛かってもいいんだけど、どっちかというとフレームワーク作りに興味が向き始めた。livedoorの各種サービスのAPIをCocoaから簡単に使えるようにするフレームワークでも作ろうかなぁ。うふふ。

ここのところ久しくObjective-Cをいじってなかったので、急にCocoaと戯れたい衝動に駆られた。そうだ、iPhotoライブラリのExporterを作ろう、と思い付きでいろいろ調べてみた。iPhotoとかろくに触ったことないけど、Objective-C+Cocoa環境だと、動的にロードしたバンドルでランタイムのクラスを書き換えるとかもできてしまうので、既存のアプリに新しい機能を付けるとかが比較的簡単にできる。その気になれば(Cocoaアプリでさえあれば)どんなものにでもプラグインを作れるので、まぁなんとかなるだろと楽観的発想をしてみる。

で、見付けた参考になりそうな情報はitok’s Labさんとこの一連の記事とそのリンク先。iPhotoには組み込みで写真のExporterを追加できる機能があるらしく、思ったより簡単にできそう。大雑把に言うと、「Exporterのプロトコルに準拠したバンドルを作って、拡張子を.iPhotoExporterにして、/Applications/iPhoto.app/Contents/PlugIns以下につっこむ」などすればいいらしい。

元記事は詳細に作り方が書いてある上サンプルコードもあって素敵なのだけど、上で挙げた記事の通り、若干内容が古い。Leopard+iPhoto'08環境で使えるプラグインを作りたいので、自力でclass-dumpを使ってクラスを調べあげてExportPluginProtocolのヘッダファイルを作るところから挑戦することにする。

が。

これがなかなか厄介で、まずここで挫折しかけた。ヘッダファイルを作りはじめてはみたものの、何かサンプルコードの時点から大分増えたり減ったりしてて何が必要なのかわからないし、クラスやプロトコロルの定義はともかくCの構造体の定義とかまではちゃんとはdumpできない。途方に暮れかけながらExportPluginProtocolとかExportImageProtocolとかでググってたら、GoogleCodeにiPhotoPluginのプロジェクトがあるのを見つけた。ひゃー。欲しかったヘッダファイル全部ある。現在の環境で動くサンプルが作れる。これは素敵。

お陰で「書き出し」メニューの中にカスタムビューを追加するだけのプラグインを作れるところまでは出来たので、あとは写真共有サイトのAPIを叩くロジックを書くだけ。これは単にHTTPでGETとかPOSTとかするだけなのでそんなに難しくない。ほほぅ。やっぱりCocoaは面白いな。

どうにも腑に落ちない現象にでくわしたのでメモ。まず、以下のようなモジュールがあったとする。

# Foo.pm
package Foo;
use strict;

sub work {
    # なんたらかんたら
}

# Hoge.pm
package Hoge;
use strict;

sub work {
    die "ABSTRACT METHOD";
}

# Fuga.pm
package Fuga;
use strict;
use base qw(Hoge);
use Fop;

sub work {
    my $self = shift;
    my $foo = Foo->new();
    $foo->work();
    # なんたらかんたら
}

要点をまとめると、

  • Foo、Hoge、Fugaがある
  • FooもHogeもFugaもworkメソッドがある
  • Fugaはuse baseでHogeを継承していて、workメソッドをオーバーライドしてる
  • Hoge、FugaとFooの間には何の関係もないが、Fugaの中でFooを使ってる
  • use Fooのところをtypoしてる(ちなみにFop.pmなどというファイルはない)

という状況があるってのが前提。俺は何をしたかったかというと、$fuga->workってやるとFooを使った何らかの処理をして欲しかった。ところがuse Fopとかしちゃってる為、これはエラーが出て実行できないコードだった(ということに気付いたのは実は大分後の話なんだけど)。これだけなら大した問題じゃない。

ところが、実際にどんなことが起きたかというと、こうだった。

  • Foga.pmのuse Foo以降のコードが無視されるが、この時点ではエラーが出ない
  • Hoge::workが実行され、"ABSTRACT METHOD"というエラーメッセージを吐いて死ぬ

何でこんな挙動になるのか理解が出来なかった。useのところでエラーが出て死ぬんならまぁ、「まーたtypoってるよ、俺ってばドジっ子で低能でワーキングプアだなぁ、あはは」でいい話なんだけど、同じくHogeをuse baseしてる別モジュールは全く何の問題もなく動き、にもかかわらずFugaは自分のworkを無視して親であるHogeの方を読みに行くってのが意味不明で、そのせいでtypoだって気付くまでに時間がかかってしまった。

結局、typoを直したら期待した通りに動いたし、そもそも設計が微妙なので後で直そうとは思ってるんだけど、なんつーか、気持ち悪い。すごいもやもやした感触が残る。うーん?なんだこれ?TheSchwartz絡みなのかなぁ、この妙な挙動。あとで調べてみよう。

朝出社したら社内ブログが出来たとのお知らせが届いてた。livedoorBlogと(ほぼ)同等の機能を持つものがイントラで動いている。

こりゃいいや、と早速記事を書いたりしてたのだけど、あちこちから「新着確認できない」「reader欲しい」との声が聞こえて来た。こういうときこそオープンソース版Fastladderの出番ですぜ。とはいえ、社員の人数分Fastladderが動いてると言うのもなんなので、社内の実験用サーバを借りてインストールしてみることにした。

まっさらなサーバに入れるので、まずは必要な環境を整える。何はともあれruby。1.8.6p-111をインストール。勢いで1.9.0を入れてやろうかとも思ったけど、今回の趣旨は実験じゃなくて「まともに使う」なので流石に自重した。gemは最新版の1.0.1を普通に入れる。Fastladder本体はパッケージ版ではなくtrunkを使う。こっちではクローラがメモリリークするとかフォームのバグでインポートが出来ないとかの不具合に対応してあって、若干安定性が増している(多分)。

若干ハマったのはFreeImage。Mac/Win版はバイナリがあるんだけど、それ以外のプラットホームでは自分で入れなければならない。今回はXen上のCentOSに入れたんだけど、パッケージが見当たらなかったのでソースからビルドしてみた。ここで容量が足りなくてコンパイル中に止まったので確認したら、ディスクが2GBしか設定されてなくて噴いた。容量設定し直してもらって再挑戦。

Fastladderの必須gemパッケージはrails2.0.2、rfeedfinder、feed-normalizer、opml、あとDBのドライバとしてsqlite3-ruby。だけど、今回は加えてMySQLのドライバとthinを入れた。多人数で共有しようと思ったらsqliteは流石にキツい。thinはrails用のサーバで、mongrelより軽量で速いと聞いたので試しにこっちにしてみた。これはただの趣味で、mongrel+mongrel_clusterでも別に良かった。上手く行ったら今後何かで使うかもしれない。

これで一応動かせる環境が出来たのだけど、さらにフロントにApache2を立てて、mod_proxy_balancerで3つ位立ち上げたthinサーバに分散させてやることにした。社内とはいえ流石に単体で動かすと遅いだろうし、クラスタリングするとまだ見ぬバグが炙り出される可能性もあったので。

ここまでやって、さぁ!みんな使え!ってところで、みんなに発表する直前でCTOから「イントラにLDR立てました」ってアナウンスがあった。…そうだよね。自社だもんね。LDR使えばいいよね。…うん。

というわけで、LDBlog+LDRの快適社内ブログ環境が整い、Fastladderは寂しくユーザ数1人(俺)のまま稼働しているというお話でした。どうしよう。1人で使うか。Faulladder。

Objective-Cランタイムを使うように書き換えられたMacRubyとやらが出たそうなので試しに入れてみた。目的としては今RubyCocoaが抱えている問題の解消、およびOSXのより深い部分までRubyで扱いやすくする、ってとこなんだろうか。

何が違うの?

  • 実装が1.9のものになっている(Leopard標準搭載のRuby+RubyCocoa環境は1.8.6)
  • メモリ管理をObjective-CランタイムのGCを使うようにしている
  • Cocoaとのブリッジ機能を使うのに特別なライブラリをrequireする必要がない
  • Frameworkの読み込みはKernel#frameworkで
  • 全てのRubyクラスはNSObjectのサブクラス
  • もっといろいろ違うとこあるんだろうけど、まだちゃんと見てないのでこんな感じ。

例えば、現行のRuby+RubyCocoaだと、

require 'osx/cocoa'
OSX.require_framework 'coredata'

class AppController < OSX::NSObject
# ほげほげ
end

とかしてるところを

framework 'coredata'

class AppController
# もともとNSObjectのサブクラス
end

と書けるようになってた。

ObjectがNSObjectのサブクラス

Rubyの全てのクラスはObjectクラスから派生しているのだけど、MacRubyではNSObjectがObjectクラスのスーパークラスになってた。

$ ruby --version
ruby 1.8.6 (2007-09-24 patchlevel 111) [universal-darwin9.0]
$ irb
>> Object.ancestors
=> [Object, Kernel]
>> require 'osx/cocoa'
=> true
>> OSX::NSObject.ancestors
=> [OSX::NSObject, OSX::OCObjWrapper, OSX::NSKeyValueCodingAttachment, OSX::NSKVCAccessorUtil, OSX::ObjcID, Object, Kernel]
$ ruby --version
MacRuby version 0.1 (ruby 1.9.0 2008-02-18 revision 0) [i686-darwin9.2.0]
$ irb
>> Object.ancestors
=> [Object, NSObject, Kernel]
>> NSObject.ancestors
=> [NSObject, Kernel]

ということはつまり、MacRubyでは全てのクラスはCocoaのクラスであって、RubyCocoaで言うところのOSX::NSObjectから使えるメソッドが全てのクラスで使えるということか。しかしObjectよりNSObjectのが上位にあるってなんか気持ち悪いなぁ、ちょっと。

実装はあとで見る

Objective-Cってことなんでソースが.mのファイルがいっぱいあるんだと思ったんだけど、流石にそういうわけではなくて(Objective-Cで全面書き直ししたわけではなくて)、大体普通のRuby1.9っぽかった。まぁそりゃそうか、わざわざObjCで全面書き直しする意味もないよなぁ。あとで何がどう違うのかちゃんと見てみよう。読める範囲で。

この間Cocoaセミナーに行って来たときに、面白い現象を見つけたので忘れないうちにメモ。

最近はもうずっとことえりを使わずにAquaSKKを使っている。SKKに慣れてしまうともう他のIMEを使う気になれないのだけど、Unixは言うにおよばずMacでもWinでもSKKがあるので、どこでも同じ操作感で日本語入力が出来て快適!もう手放せない!

Leopardにアップグレードしたときは若干不安ではあったんだけど、今日に至るまでこれと言った問題もなく動いて来たので特に気にせず使ってきた。

GCを有効にするとあれ…?

ところが、こないだついにAquaSKKが使えない事態に遭遇した。Cocoaセミナーでは演習と称してRSSリーダーを作っていたのだけど、Xcodeのコンソールでログを見てたら、どう見ても自分のアプリと無関係なエラーログが表示された。

2008-02-23 15:40:44.392 CoreRSSReader[625:10b] Error loading /Library/InputManagers/SIMBL/SIMBL.bundle/Contents/MacOS/SIMBL:  dlopen(/Library/InputManagers/SIMBL/SIMBL.bundle/Contents/MacOS/SIMBL, 265): no suitable image found.  Did find:
	/Library/InputManagers/SIMBL/SIMBL.bundle/Contents/MacOS/SIMBL: GC capability mismatch
2008-02-23 15:40:45.051 CoreRSSReader[625:10b] Error loading /Library/Components/AquaSKKInputMethod.component/Contents/MacOS/AquaSKKInputMethod:  dlopen(/Library/Components/AquaSKKInputMethod.component/Contents/MacOS/AquaSKKInputMethod, 262): no suitable image found.  Did find:
	/Library/Components/AquaSKKInputMethod.component/Contents/MacOS/AquaSKKInputMethod: GC capability mismatch

んん?SIMBL?AquaSKK?なんでなんで?

もちろんセミナーでSIMBLでプラグインを使うような高度なものを作るわけないし、テキストフォームだってIBでNSTextFieldを使ってるだけで別にオレオレカスタムViewでも何でもない。で、よく見たらGC capability mismatchと。ああなるほど。セミナーで作ったアプリはGCを有効にしてコンパイルするように言われたのだけど、SIMBLやAquaSKKはGC無効でコンパイルしてあるはずなので、それで読み込めてなかったのか。てことは、SIMBLやAquaSKKをGC有効にしてコンパイルすれば不整合が起きず普通に日本語入力できるようになるわけだ。

あれ、でもさぁ

確かLeopard発売当初はSIMBLが使えなかったのだけど、今はSIMBLもLeopard対応してるし、AquaSKKは当初からSafariやMail.appでも日本語入力できてた。てことはLeopardの標準アプリはGCを使ってないということだろうか。じゃあ、逆にAquaSKKをGC有効でコンパイルすると普通のアプリで使えなくなりそうだけど、その辺どうなんだろう。あとで調べてみるか。

feedが登録出来ない。addをクリックして、URLを入力するのだが

"You need to sign in to Fastladder

Please reload the browser and sign in again"

といったエラーが出る。

Fastladderを使って、5分でアンインストールした

すいません、これはaddフォームのバグです。ちなみに、OPMLのインポートフォームでもこの現象が起きているはずです。

どういうことかというと、Rails 2.0からのセキュリティ周りの拡張に起因します。Rails2.0ではprotected_from_forgenyとすると、POSTリクエストを送るときにセッションのトークンが含まれていない場合リクエストを拒否するようになって、これがFastladderでも有効にしてありました。で、Rails 2.0系でform_tagとかを使うとその辺よしなにやってくれるんですが、インターフェース部は大部分が本家Fastladderからの流用なのでその記述がなかった。そのため、addフォームからのリクエストが送られてきたときに不正なリクエストと判断してしまい、ログインしてくださいという、ユーザからしてみれば「??」なエラーメッセージになってしまっていました。

これはtrunk版では既に対応済みで、パッケージ版の0.0.3リリースに含めますが、差しあたっての対策としては、app/controllers/application.rbの以下の部分で

# Filters added to this controller apply to all controllers in the application.
# Likewise, all the methods added will be available for all controllers.

class ApplicationController < ActionController::Base
  include AuthenticatedSystem
  #protect_from_forgery こいつをコメントアウト
  helper :all # include all helpers, all the time

と、この機能を無効にしてやると、addやimportが出来るようになります*1

もともとmod_perlとSledgeで書いてるものをRailsで書き直したものなんで、多分この手のインターフェースとバックエンドの齟齬とか、Railsのお作法にのっとってないとかのバグはまだちらほらあるんだろうなぁ。早急にリファクタ作業に入りたいところです。

追記

と思ったけどOPMLのインポート出来なかった。formで送ってるパラメータとcontrollerが受けとってるパラメータが違うし。そういえば直した記憶あるなこれ*2

=== app/views/reader/index.rhtml
==================================================================
--- app/views/reader/index.rhtml        (revision 4072)
+++ app/views/reader/index.rhtml        (local)
@@ -392,7 +392,7 @@
                                <b>file</b> : <input type="file" name="opml" style="width:230px"> <input type="submit" value=" Upload " style="width:90px">
                        </form>
                        <form target="_blank" action="/import/fetch" method="post" enctype="multipart/form-data">
-                               <b>OPML URL</b> : <input type="text" name="opml_url" value="http://" style="width:280px"> <input type="submit" value=" Import from URL " style="width:90px">
+                               <b>OPML URL</b> : <input type="text" name="url" value="http://" style="width:280px"> <input type="submit" value=" Import from URL " style="width:90px">
                        </form>
                        <div style="float:left" class="discover_help">
                                <a href="/import/"><img src="/img/icon/new_window.gif" border="none"></a>

こんな感じです。あわわわ。テスト書こうテスト。はやくリリースしよう。

*1:社内でCSRFとかやられる心配がなければね!

*2http://code.google.com/p/fastladder/source/detail?r=23

いわずと知れたlivedoor Readerの英語版*1であり、malaさんの卒論としても有名なオープンソース版のFastladderのコミット権をもらいました。

やりたいこと、やってくれと言われてることは色々あるので、これからちょこちょこといじって行きます。

*1:まぁ、「サービスとしてのFastladder」と「アプリケーションとしてのFastladder」は別モノなんですが

CoreDataにつっこんであるデータを表示するViewを作ろうといろいろ試行錯誤してて、せっかくCoreDataでデータを管理してるのにいちいちそれを操作するコントローラを作るってのもなんだか泥臭いなと思ってたんだけど、調べたらちゃんとそれ専用のViewがあった。それがNSCollectionView。

使い方は簡単

  1. InterfaceBuilder上でNSCollectionViewを配置すると、一緒にNSCollectionViewltemとNSViewが作られる。
  2. NSCollectionViewのBindingを設定する。
    1. 「Content」を(使いたいCoreData entityを管理している)NSArrayControllerのarrangedObjectにBinding。「Model Key Path」は空のまま。
    2. 「Selection Indexes」をNSArrayControllerのselectionIndexesにBinding。「Model Key Path」は空のまま。
  3. 「Collection View Item」は既にOutletが設定されているので、特に変更することはない。
  4. Viewをいじる。
    1. 自動で作られた「View」はNSViewのインスタンスなので、カスタムビューを使う場合はこれのクラスを変更しておく。
    2. 「View」に表示したいものをぽんぽん置いてく。
    3. 配置した各項目のBindingを設定する。
      1. 「Collection View Item」にBindingする。「Model Key Path」は「representedObject.属性名」になる。

こんな感じで設定をすると、「CoreDataからデータをとってくる」→「そのデータをビューにはめこむ」→「スクロールビューに表示」までやってくれる。簡単簡単。ここまででコーディング一切なし。素敵だぜInterface Builder。残念ながらこれを使うとLeopardでしか動作しなくなっちゃうのだけど、これはかなり強力!

CoreDataとCocoaBinding

情報が少ないもんで何か難しいものと思われて敬遠されがちなんだけど、慣れてしまえばこれほど楽なものはない。データの扱いだとかUIの構築なんていう面倒なところを殆ど自作しなくてよくて、しかもそれが標準で使える機構だけでできるってのが素敵だよなー。

追記

参考にした記事を書いた人もTwitterクライアントを作ろうとしてCoreDataを弄ってらっしゃるのね。

正月休み中に形にするぞと息巻いてRubyCocoaのtwitterクライアントを作ってるのだけども、主にtwitterと関係ないところでハマってる*1。モデルの管理とIBとの連携が楽になると思って当初からCoreDataを使ってるんだけど、結構煩雑というかやぼったいというか。オブジェクトの追加は楽にできるんだけどオブジェクトの取得が面倒臭いんだよなぁ。

毎回NSFetchRequestを作ってNSEntityDescriptionを取得して…という処理を書くのがダルかったので、AR風のラッパを作ってみた。ベースのクラスを継承してモデル名と同名のクラスを作ると、そいつの操作が裏でCoreDataの操作になってるようなやつ。findとかdeleteとか、あとアクセサメソッドは定義しないでも勝手に拾ってよきにはからったりするとか、その辺は実装した。うーん、でも、このアプローチに自信が持てない。なんか車輪の再発明な気がしてならないし、多段ラッパになってて処理の効率悪そう。まぁ、勉強目的だから再発明上等だしtwitterクライアント程度でそんなにパフォーマンス気にすることない気もするけども…。RubyCocoaのdefine_wrapperとかARの拡張とかその辺りを補完するようなフレームワークを作れればいいんだけど。

こないだの補足

「CoreDataで管理してるデータは、正常終了時に自動保存されるのだけど、どこでどう処理されてるのかわからない」

テンプレートで自動生成されたAppDelegate.rbの中読んだらちゃんと書いてあった。applicationShouldTerminateメソッドの中で、managedObjectContextにsaveメッセージを送ってるのがそれですね。そこ以外ではデータの保存処理をしてないので、長時間つけっぱなしにしといて異常終了したりするとその間のログが全部ふっとぶのでどこかで定期的にsaveするようにしとかないとだ。まぁ、そもそも保存されなくても大して困らないけど。使えるものは使っておけと。

IBのチュートリアル

書こうと思ってたけど、むしろ俺自身が嵌ってるので書けそうにない。カスタムビューの作り方とか画像の扱いとかわかんないことだらけ。Objective-CやRubyCocoaはわからなくても、昔の資料から類推したりソース読んだりすればなんとでもなるんだけど、InterfaceBuilderの使い方は独力ではキツい。バージョン違うと資料が役に立たないし。今他のアプリのソース見て参考にしてるけど、誰か詳しい人がいたら助けて欲しいっす。

キーバリューコーディング

主にMVCのうちのCとVのバインディングに活躍してるキーバリューコーディングだけど、RubyCocoaと結構相性がいい。ハッシュかなんかをeachで回して全部ObjCのオブジェクトにobjc_send('setValue', value, 'forKey', key)とかしてやるだけでいいので、ラッパを作るのがすごい楽だった*2。素敵素敵。

いいから頑張れ俺

とかなんとか文章でいくら書いてもしょうがないので、作ったもの晒してみんなにツッコミ入れてもらった方が早い。さっさと投下しよう。

*1:二重の意味で

*2:具体的に言うとARでいうところのcreateあたりがまさにそう。あと、method_missingを捕捉してdelegate_object.objc_send('setValue', *args, 'forKey', name.to_s)とかやると後はよきにはからってくれるので、その辺も大して考えずに書けた。

RubyCocoaを極めるプロジェクト中でtwitterクライアントを作るという話が出てたので、正月休み中にみんなでいじって遊べる程度に動くものを作ってしまおうと目下がりがり書いてる最中なのですが、なかなか楽しいね、RubyCocoa。

e84fa15a.png

CoreDataが素敵

折角なのでCoreDataを活用してやろうと、HMDT本やリファレンスと格闘してました。CoreDataってなんぞ?ってレベルからのスタートだったので結構苦労したのだけども、CoreDataってのは要はCocoaアプリ中で簡易DBとO/Rマッパーを使えるようになるフレームワークなんだね。ActiveRecord+pstoreやsqlite、みたいなもんだと言っていいのかな。

で、これが非常に便利。Xcodeのモデリングツールでデータの定義をしておけば、勝手にDBとラッパオブジェクトを作ってくれるのでモデルクラスのとこのコーディングが格段に減る。モデル同士の関連もモデリングツールで作れるし、InterfaceBuilderとの連携でコントローラ部分も自分で作る必要がないので、簡単簡単。RubyCocoaから使う場合はCocoaから使う場合と若干構成が違うので最初躊躇ったけども。自分へのメモの意味でも、Xcode3でRubyCoca-CoreDataアプリを作る際のIBチュートリアル記事をあとで書いとこう。

ちなみに、twitterのデータの読み書きはこんな感じ

まずはデータの新規作成。

# NSManagedObjectのオブジェクトを新しく作ってDBに突っ込む
user = OSX::NSEntityDescription.objc_send("insertNewObjectForEntityForName",
            "User",
            "inManagedObjectContext",
            @managedObjectContext)
user.userId = '11111111'
user.screenName = 'rucotan'                                               
user.name = 'るこたん'

これでるこたんの情報がCoreData経由で使えるようになる。実はこの時点では「るこたんの情報が管理対象になった」だけで、自分でデータの保存処理をするか、CoreDataApplicationテンプレートからプロジェクト作ってるとアプリの正常終了時に自動で保存処理が呼びだされる*1か、そのタイミングで初めてファイルやDBに書き込まれるんだけど、「管理対象になった」時点でアプリ全体で使えるデータになるのでここではあんまり気にしない。

ちなみに、以前はNSManagedObjectのプロパティをいじるのに毎回valueForKeyやsetValue_forKeyを呼び出してたみたいなんだけど、RubyCocoaの改良でラッパオブジェクトを作るようにして、NSManagedObjectをまるでRubyのオブジェクトのように扱えるようになったらしい。軸がぶれてない、素敵*2。この辺がARっぽいとこ。

続いて既にあるデータの取得。

# NSFetchRequestを用意。RDBMSにSELECTクエリを発行するみたいな感じと言えばいいかな。
request = OSX::NSFetchRequest.alloc.init

# モデルを設定。SELECT文のFROMを指定するのに相当すると理解。
entity = OSX::NSEntityDescription.entityForName_inManagedObjectContext('User', @managedObjectContext)
request.setEntity(entity)

# 述語を設定。SELECT文のWHERE句相当、かな。
predicate = OSX::NSPredicate.predicateWithFormat('screenName == %@', 'rucotan')
request.setPredicate(predicate)

error = OSX::OCObject.new
users = @managedObjectContext.executeFetchRequest_error(request, error)

これで「screenNameがrucotanであるUserのリスト」を取得できる。NSFetchRequestとかNSPredicateってのが要はDBハンドラにプリペアードクエリを渡すのと手順は大して変わらないので、日頃DBを使うアプリに慣れてると違和感ないですね。注意点としては、executeFetchRequestの返り値はOSX::NSCFArrayなので、users.empty?とかusers.firstとかやると「そんなメッセージ、わたしが受け付けるとでも思ってるの!?この愚民は言葉もまともに話(ry」と怒られてしまう。Rubyで使うときは最初にto_aしておいた方が楽かも。

これだけだとRubyのARやPerlのClass::DBIに慣れきってる身としてはかったるくてしょうがないんだけど、Xcode上のモデリングツールで受信要求テンプレートというのを作ることができて、それを使うとNSFetchRequestの生成が大分楽になる。

variables = OSX::NSMutableDictionary.dictionary # 述語に渡すパラメータ
variables['screenName'] = 'rucotan'
request = @managedObjectModel.fetchRequestFromTemplateWithName_substitutionVariables('template_name', variables)
error = OSX::OCObject.new
users = @managedObjectContext.executeFetchRequest_error(request, error)

ちなみに、Xcode上で受信要求テンプレートを作る際には述語ビルダというのが使えて、複雑なリクエストでもiTunesやMailのスマートフォルダを作る感覚で述語を生成できてしまうので非常に簡単。だと思われる。いや、まだ使ったことないの。

キーバリューコーディング

に、ついても書こうと思ったんだけど、そろそろ長くなってきたのでまたの機会に。

仮名のRuchettaについて

Rubyの「る」とCocoaの「こ」がつくものがいいなあと考えて真っ先に浮んだのが「ルッコラ」だったんだけど、rucolaって開発環境があるのね、RubyCocoaの。ので、同じくルッコラを意味するイタリア語の「Ruchetta」にしてみた(安易)。ちゃんとr(uby)とc(ocoa)とt(witter)が入ったし、「ついった」と「るけった」で語感的にも悪くないかなと思ったんだけどどうだろう。みんなでいじれるようになったら名前も新しく考えてもらってもいいかな。

先を越された

これを書いてる最中に夏ライオン(OSXのtwitterクライアント)αリリースのお知らせが…。UIかっこいいなぁ。参考にさせてもらおう。

追記

夏ライオン見易くてなかなか良かった。自分でtwitterクライアント作ってる最中なのに当面夏ライオンユーザになりそうな予感。

*1:未確認。アプリ終了時に保存されるのは確かなんだけど、それがどこから何を呼び出してるのか実はわかってない。あとでちゃんと見る。

*2:そういえば絶望先生2期って今クールでしたっけ?

大分放置してましたけど思い出したかのように続き。いや実際忘れてたのだけども。

とりあえず以前書いたものたち。

spceコマンドのラッパを書く

前回は自分のリソース内の*_spec.rbを読み込んで自分のmainバンドルのクラスをテストする、というRubyCocoaアプリを作ったのだけど、specを走らせるためだけに毎回そんなものを作るのは面倒くさすぎる。実際やってることはSpec::Runner::CommandLineにspecファイルを渡してるだけなので、ローダブルバンドルを引き数に取ってspecを走らせる、というspecコマンドのラッパを用意する。

#!/usr/bin/env ruby

require 'rubygems'
require 'spec'
require 'osx/cocoa'

spec_opts   = ARGV.reject {|opt| opt =~ /\.bundle\z/}
bundle_path = ARGV.find {|opt| opt =~ /\.bundle\z/}

if bundle_path && bundle = OSX::NSBundle.bundleWithPath(File.expand_path(bundle_path))
  bundle.load
  bundle
  Dir.foreach(bundle.resourcePath) {|file| spec_opts.push(File.join(bundle.resourcePath, file)) if file =~ /.+_spec.rb\z/}
end

::Spec::Runner::CommandLine.run(spec_opts, STDERR, STDOUT, true, true)

見ての通り、バンドルをロードして*_spec.rbを抜き出し、それらのパスをコマンドライン引数に付け加えてSpecのランナーに渡すだけ。バンドルは最初の一個以外は全部無視する設定になってるけど、俺が想定してる使用方法で扱うバンドルは一個だけなのでとりあえずこれで十分。必要であれば全部受けつけてもいいけど。

拡張子が.bundleのファイル以外の引数は全てspecのランナーに渡るので、適当な名前付けてパスの通ったとこに配置しておくとspecコマンドの代わりに使える。-fとか-cとか-sとかも問題なく使えたりする。

使用方法

で、こいつを実際にどう使うかというと、例えばXcodeを使っているならこんな感じ。

  1. 新規ターゲット追加でCocoa Loadable Bundleのターゲットを追加
  2. そのターゲットに次のスクリプトを実行するビルドフェーズを追加:ruby <上のスクリプトのパス> [<specオプション>] $BUILD_DIR/$CONFIGURATION/$EXECUTABLE_NAME.bundle
  3. そのターゲットにObjective-Cのクラスとspecファイルを入れる(その際specファイルは必ず末尾に「_spec.rb」を付ける必要がある)

これで、このターゲットをビルドすると自動的にspecを実行してくれるようになるという噂。いや、ちゃんとなりました。なりましたよ。スペックが全て通ると何事もなくビルド完了するけど、スペックが通らないとビルドが失敗しビルド結果画面にspecコマンドの結果が出る。例えばビルドターゲットの依存関係でアプリターゲットがspecターゲットに依存するようにしておくと、specが全て通らない限りアプリがビルドできなくなるのでいい感じ。

注意点いくつか

まず当然のことながらRubyCocoaがインストールされてないと動かない。Leopardだと最初から/System/Library/Frameworks/RubyCocoa.frameworkがあるんだけど、それはもともと入ってるruby(/usr/bin/ruby)からしか使えない。普段から/usr/bin/rubyを使ってる人は別に問題ないけど、それ以外*1を使ってる場合、そのRuby用に再度RubyCocoaを入れるか、/usr/bin/rubyの方でRSpecを入れるかしておく必要がある。

あと、RSpecの最新版だとspecの実行結果をsuccessとfailedの他にpendingにすることができるけど、pendingはsuccessと同じ扱いになってビルド成功してしまう。ので、ご注意を。まぁ、実行結果はビルドログに吐きだされてるのでそれを見ることはできるけども。

その他

とりあえず結果表示をどうにかしたい。Growlに通知投げたりしたら面白いかなぁ。

*1:/usr/local/bin/rubyとか/opt/local/bin/rubyとか

どう書く?.orgのお題に取り組み始めてからObjective-Cでいろいろ書いてて、久しぶりにCocoaのリファレンスを読んだりしてるんだけど、いやハマるハマる。二重の意味で。Objective-C熱が出てますよ。二重の意味で。昨日はファイル更新監視のツールに挑戦してたんだけど、随分とスットコドッコイなものを作ってました。CUIのツールなのにGUIのAPI叩いてたりとか、アホかと。それでも書くのが楽しくて時間を忘れたくらい面白かった。いいなぁObjective-C。マカーなプログラマはもっとCocoaに触れてみるといいのにな。Cocoa-Javaサポートも外れた?ことだしJavaJavaしてる場合じゃないですよ。いや、JavaはJavaで好きだけど。

どう書く?.orgに挑戦してて思ったこと

なんかお題がないと「それPRagg(ry」なものを再発明しては悦に入る程度のことしかしないので始めたのだけど、やり始めたらいろいろ再認識することが多くて凄く楽しかった。そもそもなんでObjective-Cで書いてんのかというと、今ならObjective-Cで書いてると目立てるんじゃね程度の中二病的短絡思考に過ぎないのだけども、図らずも仕事でも趣味でもやってこなかった種類のプログラミングだったのでものすごく勉強になる。

言語ごとの向き不向きを肌で感じる

どう書く?.orgのコンセプトや投稿してるユーザ層を考えるとそういうもんかなとも思うんだけど、CUIでOS非依存のテキスト処理とかリスト操作のお題が多い感じがする。で、それらをObjective-CというかCocoaで書こうとして再認識したのは、その手の処理をLLで書くのがいかに楽かってこと。もともとそのためのスクリプト言語だったりするし、文字列の検索・置換だとか配列のならべかえとかがびっくりするほど簡潔に書ける。最近はWebアプリを書くのに多用されることもあってHTTP経由のリソースの扱いも言語組み込みあるいは標準のライブラリで簡単。いかにシンプルに手早く書けるかという点においてだけ言えば、もともとネイティブアプリのフレームワークであるCocoaでやろうとしたってまず敵わない。

一方でCとかLispの「何でもできるぜ」っぷりも物凄い。そりゃあ理屈で言えば、コンピュータの処理として記述できるものは(簡単か難しいかの差こそあれど)どんな言語でも記述できるとはいえ、その汎用性は流石としか言いようがない。定石も多いし。それから関数系の言語のエレガントさも惚れぼれする。いいなぁ。ああいうのをさらっと書けるようになると脳味噌の中身もすっきり整理されそうだな。

あと、秀丸マクロとかPowerShellとかの投稿が予想外に多いのが面白かった。というかObjective-Cが秀丸マクロに負けてるのはなんか意味もなく悔しいな。ユーザ数で言ったら秀丸マクロより下なんでしょうか、Objective-C。もっと頑張れObjective-C。君はやれば出来る子なんだから*1

何故かCの勉強になる

Objective-Cは見た目はアレだけど実態はCの拡張なので、素のCコードと混在できる。GUIアプリだと大分様相が変わってくるんだけど、CUIのツールを作ろうとすると、制御方法とかアルゴリズムとかはCの例を参考にすることになる。そんなこんなで、Objective-Cを書いてるつもりが気がつくとCのライブラリ探したりmanで関数の使いかた読んだりしてた。

それから、アプリケーションフレームワークであるところのCocoaはObjective-Cで書いてあるけど、それより下のレイヤーのCore FoundationライブラリはCで書いてある。なのでランタイムAPI叩いてリフレクションとか、もっと低水準で細かく制御したいとか、そういうこと考え始めると必然的にCでゴリゴリになる。まぁ、俺はまだそんなレベルでは全然無いんだけど、Cocoaのリファレンスとか読み漁ってるうちに気がつくとCでランタイムいじるような内容を読んでたりすることはままあった。

あと、C++だとありがちな話だけど、Objective-Cでも拡張子は.mだけど良く見るとまるっきりC、みたいなコードが書ける。どう書くのお題をやろうとしたときは、Cとの差別化をしないとなと思ってわざわざNSStringとかNSArrayとかに変換して処理してみたけど、正直「これCで書いた方が楽じゃね」って時々思ってしまった。もちろん本格的なアプリケーションの中でOSの機能をバリバリ使うとなったらどう考えてもCocoaのが強力で楽で保守性も上がる、はず。でも、当り前のことだけど、一個の関数の処理をラップしただけみたいな小さなツールで、かつOS非依存の処理だったりすると、わざわざCocoaで書く意味はあんまり無い。つかCどころかRubyでいい。むしろシェルスクリプトでいい。

そんなわけでCの勉強をかなりした(気分になった)。

そもそも根本的なところが弱過ぎるぞ俺

自覚してはいたけど、アルゴリズムとかデータ構造とか実行効率とかエラーハンドリングとか、土台の部分が脆弱過ぎる。他の人がやってるのを見ればなんとか、「こういう風に処理すると効率がいい」とか「これだとこんな理由でダメなんだな」とかいうのは想像できるんだけど、自分でそれを発想するに至ってない。高水準言語でWebアプリ開発ってのに慣れきってるのもそうなんだけど、そもそも勉強不足。いくらスクリプト言語が、ライブラリや処理系が裏で良きにはからってくれるとと言ったって、中で何やってるのか分かってる上であえて意識しないというスタンスでいないとロクなものは作れない。そりゃ「最近のLLかぶれの若造は使えねえ」と言われるわ。

いや、LLの凄い人だって下のレイヤーのことも良く知っているし基本的な考えかたを理解しててLLで書いてるから凄いんであって、LLだとかそうじゃないとかの問題じゃない。それなりに作れるかなと思い始めた*2ミユビナマケモノがより高みを目指すには、一度きちんと基本に立ち返る必要があるのを再確認というか痛感した。おっかしいな。Cのソートアルゴリズムとかバイナリサーチとかリスト構造とかを始めて見たときは感動してたんだけどな。所詮ナマケモノ、あのときの情熱が自分の能力の向上に繋がってねぇ。

TODO

  • iPod touchでタイトなリソースで動くプログラムを作る
  • 一瞬でも落ちたら大量の嫌がらせメールが俺の携帯に届くようなサーバアプリを作る

もっと緊張感を必要とするプログラミングをしてみようかと思いました。そうじゃないとずっと「それなりに動くが、それなりでしかない」ものを作り続けてしまいそう。

  • どう書く?.orgでもRubyやObjective-Cだけじゃなくて他言語のコードも読む
  • 仕事中の待機時間に他のプロジェクトのコードを読む、Sledgeのソースを読む
  • オープンソースのコードを読む

読んで理解できるということは自分でも書けないわけじゃないんだよなぁ、多分。きっと。もっと大量にInputすれば多少はOutputのセンスが磨かれるかなぁと。あとさりげなくどう書く?.orgにObjective-Cのコードをばんばん上げ始めてくれたPsychsさんに多謝。読みまくったる。

*1:余談だけど、俺高校時代に「faultierってやればできるのに絶対やらない子だよね」って言われた。多分、全く褒められてない

*2:勘違いともいう

↑このページのトップヘ