RubyMotion を1年以上使い続けてみての雑感

RubyMotion Advent Calendar 2013 に何か書こう、ということでエントリ。

ご存知のように iPhone アプリの HBFav は RubyMotion で作っています。Objective-C ではなく。以前は Titanium Mobile で作っていましたが、去年にバージョン2として作り直すにあたって RubyMotion に移行しました。

RubyMotion に関しては以前、以下のエントリで概要を説明しています。

それから、今年 5月に開催した RubyMotion カンファレンスのスライドなどもあります。

RubyMotion が発表されたのは 2012 年の5月 とかで、それからずっと使い続けているので1年半近くが経ったことになります。App Store に配信しているアプリを、それなりの期間継続的に開発してきて知見も溜まってきましたし、ここで RubyMotion を使い続けての実感みたいなものを述べていこうと思います。

パフォーマンス、アーキテクチャ的な点について

RubyMotion を使って個人的に最も良かったことは、やはりパフォーマンスです。まあ Objective-C で普通にネイティブアプリを作っている場合とはパフォーマンスは変わらないのですが、自分の場合は比較対象が Titanium Mobile だったので、ここは大きく改善しました。

なぜ RubyMotion は (一般にネイティブな言語に比べて遅いという印象がある Ruby であるにも関わらず) 速いのか。そしてその速度は Objective-C での実装とほぼ差がないのか。

これは、RubyMotion の説明になると避けては通れない話であり、また最も RubyMotion について誤解を招きやすい部分でもあります。

RubyMotion は Objective-C のジェネレーターではありません。 RubyMotion は、RubyObjective-C のコードに変換してそれからアプリをビルドするのではなくRuby のコードを直接 LLVMバイトコードコンパイルし、それを Objective-C ランタイムや Cocoa フレームワークとリンクしてアプリをビルドします。このとき重要なのは、Rubyバイトコードコンパイルされるということではなく「Objective-C ランタイムや Cocoa フレームワークと直接リンクされる」という点です。

RubyMotion は実は、RubyiOS アプリを作れるといっても実際にはかなり薄い実装で、Objective-C という言語が Objective-C ランタイムシステム (その実体は C言語関数群そのもの) の API を呼ぶ言語であるのと同様に、Ruby という言語で Objective-C ランタイムシステムAPI を呼んでいるだけの処理系なのです。(それから、RubyMotion の型も Core FoundationCocoa Foundation と同じように、Toll-free bridge です。)

そして、iOS アプリ開発というのは、実際には Objective-C によって iOSフレームワークAPI を呼び出し、その組み合わせによってアプリを構成することにほぼ等しい。最近の Webアプリケーション開発などもそうですが基本的に自分でプリミティブなロジックを組むということはほとんどなく、基本的にはライブラリやフレームワークが持っている API を呼んで、実際の計算処理のほとんどはそのライブラリ/フレームワーク側が担当することになります。

従って、Objective-CiOS アプリを作った場合でも、RubyMotion で作った場合でも、その計算処理のほとんどは Objective-C ランタイムおよび Cocoa フレームワークの内部で行われるので、その両者にパフォーマンス的な違いが出ることはほとんどありませんし、同様の理由から、挙動的な部分でも差異が出ることもまずありません。すなわち、速度面でも機能面でも、Objective-C で開発したときに出来て RubyMotion では出来ないことは(ほぼ)ないと言い切れるのですね。

iOS アプリを Ruby で作れる」というとなんとなく Ruby という言語処理系あるいはエコシステムがあって、それらを使って iOS アプリを作れるという印象が先行しますが、実際には Objective-CCocoa フレームワークのテクノロジースタック上で、言語のシンタックス部分だけが Ruby に置き換わったもの、という方が RubyMotion を正しく捉えた説明かなという気もします。実際、Ruby インタプリタガベージコレクションその他、普通の MRI Ruby の実行に必要なコア部分なんかは RubyMotion には含まれていません。

このアーキテクチャ的な特徴からくるパフォーマンスの良さや、Objective-C + Xcode な実装との互換性の高さの部分が、RubyMotion を使っているときに最も嬉しいと感じられる部分で、この観点についての RubyMotion の評価は概ねポジティブです。

Ruby 的な表現力について

このように RubyMotion は Objective-C Runtime の上に乗った薄い実装なのですが基本的なオブジェクト、コレクション、正規表現などに関しては RubyMotion 側で追加の実装が施されており (これも最終的には NSObject や NSArray、NSDictionary の API を呼ぶのですが) 、おかげで、シンタックス的には普段の Ruby の表現力が活かせます。クラス・モジュール構文や Mixin なんかも同じくです。

例えば配列を操作したいときには

@bookmarks << bookmark
@bookmarks.uniq! { |bookmark| bookmark.id }
@bookmarks.each do |bookmark|
  ...   
end

といつもの Rubyシンタックスで書くことができ、且つこれが100% ネイティブのコードになる。Objective-C の NSArray や NSDictionary の API だけで複雑なコレクション操作を行おうとすると気が滅入ることもありますが、その辺の負担がかなり軽減されます。派手な機能ではないものの開発しながら、地味にありがたいなあと思わされることが多いです。

一方、繰り返し述べたとおり RubyMotion は言語のシンタックスだけが Ruby になった iOS SDK みたいなものですから、こういったコレクション操作等以外の普段のコードは

def applicationWillEnterForeground(application)
  notify = NSNotification.notificationWithName("applicationWillEnterForeground", object:self)
  NSNotificationCenter.defaultCenter.postNotification(notify)
end

こんな感じで Cocoa Touch の API を呼ぶ、あまり Ruby らしくないコードを書くことになります。

当然、折角なので Ruby らしく書きたいと思う人は世の中にはたくさんいて、bubble-wrap や sugarcube、あるいは ProMotion といった、Ruby らしいシンタックスに置き換えるシンタックスシュガーライブラリが RubyMotion 用の gem として、結構活発に開発されています。この辺を使ったときにビフォア、アフターがどうなるかは ProMotion - naoyaのはてなダイアリー あたりにも以前に書きました。

それで、自分も最初は bubble-wrap や sugarcube なんかを積極的に導入して、なるべく Ruby っぽく書けるようにとやっていたのですが、これは正直失敗だったかなあというのが今となっての感想です。

確かに bubble-wrap や sugarcube で長い APIRuby らしく置き換えることはできるのですが、ある程度以上の規模のアプリを作っているとそれらライブラリがカバーしていない API 領域の API を使う頻度が増えてくる。そうすると、同じコードの中に、一方では Ruby らしくしようと頑張ったコード、一方は Cocoa Touch なコードと二つの体系が混ざってしまう。よかれと思って Ruby らしいスタイルを取り入れようとした結果、かえって中途半端な感じになってしまいました。コードを書くにあたって「今回はどちらのスタイルで書こうか・・・」と迷いが生じ、それを明確に判断する基準を自分の中に確立することができませんでした。

結局、昨今は基本 Objective-C 的なスタイルに寄せて、Ruby らしいスタイルは敢えてあまり使わなくなりました。非同期 HTTP リクエストを簡単に書ける BW::HTTP や、あまり大袈裟でない sugacube の一部シンタックスシュガーのみ控えめに使う・・・といった具合です。

最初は長ったらしい Cocoa フレームワークAPI をなんとか短い表現にラップしてから書いてやろうと思っていたのですが、慣れてくると、その長い APIAPI の動作をきちんと自己記述的に説明しているものであることが分かってきて、案外悪くもないなと思うようになったという心境の変化も大きかったと思います。いろいろすみませんでした。

サードパーティライブラリのエコシステムについて

こうして、RubyMotion の一つのウリであろう、RubyMotion 用にか書かれた rubygems ・・・ Top 10 Software Development Companies なんかでも多数公開されてますが、これらには当初期待していたほどの魅力は、自分は昨今はあまり感じなくなりました。前述の通り bubble-wrap や sugarcube のごく一部の API だけを使うに留めています。

一方で、RubyMotion は Objective-C ランタイムや Cocoa フレームワークと直でリンクするという性質上、rubygems だけでなく、Objective-C 用のサードパーティライブラリが凝ったブリッジなどを用意しなくても、そのまま使えるという特徴を持っています。Cocoapods にある実装なんかももちろん使える。

例えば以下は閲覧中のURLを外部サービスの Pocket に送る機能のコードです。

def performActivity
  SVProgressHUD.showWithStatus("保存中...")
  PocketAPI.sharedAPI.saveURL(@url, handler: lambda do |api, url, error|
      if error
        SVProgressHUD.showErrorWithStatus(error.localizedDescription)
      else
        SVProgressHUD.showSuccessWithStatus("保存しました")
      end
    end
  )
  activityDidFinish(true)
end

SVProgressHUD は Cocoapods で公開されている、画面上に半透明のメッセージウィンドウを出すことができる Objective-C ライブラリ。PocketAPI が Pocket の iOS SDK ですね。これらの API を、普通にリンクして呼び出すことができています。

RubyMotion を始めた当初は、どちらかというと機能の派手さや新鮮度から先の RubyMotion 用 rubygems に目がいっていましたが、ひとつのプロダクトを継続的に作り込んでいくとこちらの Objective-C ライブラリ群にアクセスできる、Objective-C エコシステムの資産を活かして巨人の肩に乗れる、ということの利点の方がずっと重要であることに気づきました。

この Pocket の例のように HBFav はいくつかの外部サービスを使っていますが、それら外部サービスを iOS アプリに組み込むための SDK はすべて Objective-C ライブラリとして公開されています。はてなブックマークはもちろん、Pocket、MBaaS の Parse.com、Google Analytics など。これらの SDK も、RubyMotion なら普通に使うことができます。

iOS アプリを他の言語で書く、的なものは Titanium Mobile や RubyMotion 以外にもいろいろな実装がありますが、この芸当が可能なのは、おそらく RubyMotion あるいは RubyMotion 同様のアーキテクチャを採用した実装 (例えば PerlMotion) ならではと言えます。

Objective-C でいいのでは? 論

しかし、こうして見てみると

  • パフォーマンスは高速であるとはいえ、Objective-C で普通に書いた場合とは、当然同じ
  • RubyMotion 用の rubygems は最初は有り難かったけど、使い込むうちに限定的にしか使わなくなった
  • 無理に Ruby 的なスタイルを採用するのはやめて、Objective-C スタイルに寄せるようになった
  • Cocoapods を初めとする Objective-Cサードパーティを使うことができる、とはいえこれは Objective-C であれば当然のこと

という感想になっていて、合理的に考えると、Ruby 組み込みのクラスが便利だという意外は、普通に Objective-C + Xcode で書けばそれでいいじゃないかという結論に達しそうな雰囲気があります。Objective-C の処理系も年々バージョンアップして簡単になってきているし、RubyMotion での (Rubyらしさをあまり追求しない) コードと Objective-C で書かれた同じコードを比較しても、そこに明らかな生産性の差が現れるかというと、そんなことも全くないと思います。

しかし実際には、RubyMotion でのアプリ開発には、Objective-C + Xcode でのそれとの間に何か言語化しづらい性質の違いがあるように思います。自分はそれほど Ruby という言語に思い入れのある方ではありませんが、生粋の Rubyist が何か Ruby に「気持ちがいい」とか「楽しい」とか、個別機能論ではなく総体としての Ruby にそういう体験を感じたりするように、RubyMotion にもまた、個別の事象だけを切り取って議論したときにはわからない手触り感の良さのようなものがあるように思います。その手触り感の魅力に関しては一年以上使い続けても色あせないばかりか、使い込むほどますます手に馴染んでいくので、ちょっと RubyMotion なしでの iOS 開発というのは考えにくい。そんなわけで自分はこれからも、HBFav のようなライフワークには変わらず RubyMotion を使い続けようと思っています。

逆に、例えば仕事でやるプロジェクトなんかで、Objective-C ではなく RubyMotion を採用すべき! ということは、自分は主張しません。各論で見た場合にその主張に説得力を持たせることができませんし、複数人数での開発では、やはり標準的な環境、Objective-C の堅牢さみたいなもののメリットの方が重要になってくると思います。その辺を十分分かった上でRubyMotion で本格的なアプリを開発しているというプロジェクト、例えば 37 Signals なんかを、面白いなあと思ってウォッチしたりはしています。

その他にもこの辺が良かった/課題だな、ということに

  • 開発元の HipByte のサポートとか、ユーザーとの近さ
  • (RubyMotion が薄い実装であるがゆえに) iOS 6 から iOS 7 へのキャッチアップに一切のタイムラグがなかったこと
  • 普段 Web 開発で使い慣れてる道具が使える
  • Objective-C と RubyMotion の間での知識移転が楽
  • 日本の RubyMotion コミュニティが楽しい (毎月のもくもく会がなかったら、途中で開発の手が止まっていたかもしれない・・・)
  • テストが書きやすい、という利点があまり活かせてない

みたいなことも挙げられますが、長くなってきたのでこの辺にしておきましょう。