現実世界のSwift ー Duolingo開発者が語るSwiftの強みと弱み

最近、私たちはSwiftベースの新しいアプリを発表しました。Appleによって派手に取り上げられ、非常に多くのユーザを獲得しています。この記事では、私たちの経験を共有し、この新しいプログラミング言語について一般的な考えを述べ、私たちのアプリをより強固なものにしてくれる、Swiftの長所をいくつか紹介したいと思います。

これはSwiftのチュートリアルではありません。この記事は、まだSwiftの経験が浅く、実際はどのように使われているのか興味を持っている開発者のために書かれています。技術的なコンセプトにも言及します。また、必要に応じて既存のチュートリアルやドキュメントへのリンクも含まれます。

まず、この新しいアプリの機能や、主な目的について簡単に説明します。

新しいアプリ

当社の主力アプリDuolingoのことは既にご存じかもしれませんね。6,000万人以上のユーザ(2014年12月現在)を獲得している人気の言語学習アプリで、AppleからApp of the Year 2013(2013年度アプリ大賞)に選ばれています。新しい言語を学びたいなら、Duolingoは頼りになる方法ですよ。iPhoneやiPadで利用できます。

これに加えて、語学力を証明するために、Duolingo Test Centerも始めました。例えばあなたが外国人で、アメリカやイギリスの求人に応募したい場合や、大学に入学したい場合などに便利です。就職や入学には、言語の堪能さを認証する公式の証明書が求められることがよくあるからです。Test Centerのユーザは自分の語学レベルを確認するために適応型テストを受験します。不正行為を防ぐために、テストは人の目で監視されます。

iOS向けTest Centerが発表されると、Appleによって50カ国以上の「最新ベストアプリ」として取り上げられました。

App Screenshot: Take Picture
App Screenshot: Speak Sentence
App Screenshot: Select Real Words

目的

Test Centerはパフォーマンスを重視したアプリではありません。ほとんどが、コントロールがいくつか付いただけの静的コンテンツです。また、不正行為防止のためにテスト中にビデオで録画されますが、その程度です。Swiftに関してパフォーマンスの問題には特に気づきませんでしたが、注意を向ける必要もありませんでした。

私たちにとってより重要だったのは、アプリの安定性と堅牢性でした。テストは20分ほどかかり、いずれは有料になる予定なので、試験中にクラッシュするようなことがあれば、極めて悪いユーザエクスペリエンスになってしまいます1。そのうえ、テストを開始したら、最後まで続けなければなりません(つまり、テストを一時停止したり、アプリを終了したりすることはできません。これは不正行為を防止するためです)。そういうわけで、クラッシュは最小限に抑える必要があります。

Swiftに関する一般的な考え

Swiftがリリースされた時、多くの人はこの言語のシンタックスを見て、比較検討し、結論を出しました。「これでもうObjective-Cのシンタックスに我慢しなくて済む、iOS開発に参加できる」と言った人たちがいます。率直に言って、これはSwiftに対する間違った見方でしょう。ある程度合理的でありさえすれば、シンタックスなんてどうでもいいのです。言語にはもっと重要な側面があります。例えば、懸念事項を簡単に表現できるとか、間違ったふるまいを助長しないとかです。

SwiftはObjective-Cや他のいくつかの言語よりも、深い悟りの道に導いてくれます。言語的には単なる一歩のように思えるかもしれません。Swiftの設計者をフォローしていれば、彼らが関数型プログラミングなど、他の領域から優れたコンセプトを借用していることが分かるでしょう。また、彼らは適当だと思えば、既存の(ただし理想的とは言えない)コンセプトを排除しています。

Objective-Cを使ったプログラミングに慣れていた私たちにとって、Swiftは素晴らしい、歓迎すべき前進でした。Haskell(または類似したもの)を使い慣れた人には、まだ改善の余地があると感じられるかもしれません。私たちは、この言語の次のバージョンがもたらす改善を大いに楽しみにしています。

長所

Swiftは、開発者が他のプログラミング言語で慣れている機能をいくつかサポートしています。例えば、カスタムオペレータや関数のオーバーロードなどです。値の型(Swiftのstructなど、値セマンティクスを持つ型)は、コードを理解しやすくしてくれます。

また、私たちはその強力な静的型システムが気に入っていて、その型推論のおかげで、使うのがとても楽しいです。ジェネリクスもObjective-Cにはなかったものです。さらに、(うまくいけばこの型のオブジェクトを含んでいる)NSArrayの代わりに、型安全コレクションを持つことができます。

それでは、私たちが本当に便利だと思った特徴について、もう少し詳しく見ていきましょう。

例外なし

これまでのところ、Swiftには例外処理がありません。Swiftの設計者があえてそうしたのか、その時間がなかっただけなのかは分かりません。しかし、例外処理がないことは、いい考えだと私たちは考えています。(非チェック)例外は、コードの理解を難しくするからです(チェック例外は例外の発生箇所をより明確にできるかもしれませんが、使用するのが厄介な場合が多く、いずれにしろObjective-Cはこれらをサポートしていません)。

実際、当社のよくあるクラッシュの原因第7位は、Apple提供の例外スローのメソッド(-[AVAssetWriterInputHelper markAsFinished])なのです。このメソッドは、例外スローとしてマークされてもいませんし、文書化もされていません。このため私たちはクラッシュの報告を聞くまで、こうしたことが起きるとは全く分かっていませんでした。気づいた時には、私たちのアプリはユーザの元でクラッシュしていたのです。

Cocoaのベテラン開発者なら、Objective-Cは例外スローや例外処理メカニズムを提供するものの、例外は通常、例外的な状況でのみ使用され、それは普通、復旧不可能な状況(これについては反例もありますが)を指すのだと指摘するでしょう。この場合の適切な解決策は、おそらく例外を@catchしないことではなく、そもそもスローされることがないようなもっと良いコードを書くことでしょう。この場合、例外はアサーションエラーのようなものだという考え方もあるかもしれません。しかしこれがこのコンセプトの唯一の用途だとしたら、assert()やfatalError()を持つ新しい言語に必要でしょうか?

一般的に、エラーケースの処理を忘れる事態は避けたいものです。理想的には、アプリを発表してからではなく、コンパイル時に全ての問題を把握しておきたいのです。例外があると、これが難しくなります。ではSwiftでエラーの可能性を表現するために、他に何を使えばいいでしょうか?

OPTIONAL

Swiftが大いに依存しているコンセプトの1つは、Optional(おそらくHaskellのMaybe型に由来するとご存じの方もいるかもしれません)です。Appleのドキュメントには以下のような説明があります。

Optionalの型は2つのケースの列挙となります。NoneとSome(T)の2つで、存在するか存在しないかが分からない値を表現する際に使われます。どんな型でも明示的に(もしくは暗黙に変換された)Optional型として宣言できます。

Optionalがスムーズに機能できるよう、Swiftは糖衣構文を提供しています。Noneの場合にはnilというような、アンラップされた特別なシンタックスや演算子などです。さらにoptional chainingを使えば、複数の依存するOptionalを含む、明確で簡潔なコードが書けます。

では、どのように使うのでしょうか。Optionalは値が無い可能性のあるケースをエンコードする優れた方法です。また、ある関数が期待した値を返さないかもしれないケースを表現する場合にも使用できます(どうして期待値が返らなかったかについては関心がない場合に限ります)。

Objective-Cでnilにポインタを設定するよりも、この方法が優れているのはなぜでしょうか。それは私たちが日頃話しているような正しい型を、コンパイラが(コンパイル時に)実行するからです。言い換えれば、Swiftにおいて非Optionalの値はnilにはなり得ないということです。また、SwiftのOptionalは単なるポインタ型よりも多くの働きをする点で汎用的と言えます。

使い勝手の良さが分かる例があります。Objective-Cでは、オブジェクトイニシャライザ(例えば-int)のようなポインタ型を返すどんなメソッドも、nil(例えばあるオブジェクトが初期化できない場合など)を返す可能性があります。具体的な例を挙げるとすれば、+ (UIImage *)imageNamed:(NSString *)name;です。メソッドの型を見るだけで、それが絶対にnilを返さないとは断言できません。

しかしSwiftではそれが可能です。Appleはfailable initializerというコンセプトを導入しました。単に型レベル上で表現する際には非常に便利です。Swiftでは、上記の例文をinit?(named name: String) -> UIImageと記述します。「?」のマークに気づきましたか? これはnameというリソース名が見つからなければ、initがnilを返す可能性があることを表しています。

私たちはここで挙げたことを全て、必要に応じて頻繁に使っています(暗黙にアンラップされたOptionalや強制的なアンラップをなるべく避けるようにしています)。ある表現でnilが生じても(例えばエラーなど)その理由を知る必要はありません。Optionalはそうしたケースに対処する優れた方法です。

RESULT

エラーになるかもしれない呼び出しを実行して、エラーとなった理由をどうしても知りたい場合は、非常に簡単で優れた型であるResult(サブ型のEitherは関数型プログラミングでおなじみですね)をSwiftでは定義できます。

Optionalとの類似点は、型レベルにおいて、Resultは与えられた型の値かNSErrorのどちらかになる、という表現ができることです。

Optionalと同じようにResultでは、Success(T)とFailure(NSError)というように、単純に2つのケースを列挙します。成功した場合は、ペイロードとしてあなたが必要な値を返します。エラーが起きなければ、つまりNSErrorの記述と共に.Failureが返されない限りはそのようになります。

Optionalと異なる点は、ResultはSwiftの標準ライブラリに含まれていないことです。つまり個別に定義する必要があります(現段階ではコンパイラの機能が不十分なので、自分で何とかしなければいけません)。

ネットワークの構築時やI/Oやコード解析の時にはResultを多用します。そのほうが、古いNSErrorのinoutポインタパターンや、成功時の値とエラー時のポインタの両方を持つ(さらに成功時のブーリアン値とNSerrorポインタの複雑な組み合わせの)completionブロックを使うよりも、ずっと簡単です。

Resultは洗練された方法であり、それによってコードをより簡潔にし、安全性を高めることができます。私たちのアプリでは、(致命的ではない)エラーとなる可能性のある表現は全てOptionalかResultのどちらかを返します。

Objective-Cとの相互連携

Swiftをデザインする際に鍵となったコンセプトの1つが、Objective-Cとの相互連携です。Appleにとって、新たなプログラミング言語を発表すると同時に、Swiftの再実装と共に大量のライブラリを全て置き換えるなどということは、とうてい実現不可能なことです。少なくとも同時には無理です。さらに、開発者コミュニティは膨大な量のObjective-Cのコードの上に成り立っています。うまく相互連携できなければ、現実的な理由からSwiftが日の目を見ることはなかったでしょう。

幸いにも、Objective-Cとの相互連携はどちらの側からでも実に簡単です。私たちが限定的な範囲で試したところ、十分に運用できました。ただし、Swiftのコンセプトのうちいくつかは(列挙など)Objective-Cで直接使うことはできません。

例を挙げてみましょう。Swiftで記述した、PDFデータを表示するというアプリのコンポーネントがあります。このモジュールをObjective-Cで記述した主力アプリで使いたいとします。ところが残念なことに、いくつかのメソッドはSwift限定の機能を使っていました。つまりこれらのメソッドは自動的には連携されないということです。こうしたケースに対処するために、私たちは単に、Objective-Cでも表現可能なラッパーメソッドを導入することにしました2

私たちの主力アプリで使っているObjective-Cの既存のコンポーネンのうちいくつかを、Swiftから使えるようにすることは簡単でした。そのためには、単にアプリ全体からコンポーネントを切り離し(既にモジュラであれば理想的です)、その後bridging headerによってSwiftにインポートします。

短所

Swiftは明らかにObjective-Cを改良したものとはなっていますが、依然として改善の余地はあります。例えば、この新たに作られた言語には、最新の型システムを備えた他のプログラミング言語にある表現力が不足気味です。しかし、まだ誕生したばかりの若い言語なので、そのうち改善されるでしょう。

Appleはサブミットされたバイナリを維持できると保証しましたが、必要に応じて言語に改変を加えるかもしれないとのことです(実際、既にこれまで何度か実行しました)。つまり、コンパイラをグレードアップした後で、コードを修正しなければいけないかもしれません。さもないと、コンパイルが通らなくなる可能性があります。実際に、このような状況はあり得ると分かっていたので、私たちは受け入れることにしました。ありがたいことに、今のところ、実働している既存のコードに対する”修正”にはあまり時間をかけずに済んでいます。

Swiftに関して最も不満な点、言わば苛立ちの原因になっているのは、おそらく言語そのものではなくて周辺のツール環境です。Xcode(AppleのObjective-CとSwiftのIDE)はまだSwiftのコードと緊密に連携が取れていません。アプリケーションの開発中にIDEの処理が著しく遅くなったり、クラッシュしたりといったことがよくありました。ほとんどの場合はコードが終了せず(または処理が非常に遅くなり)、基本的にデバッガは無く、シンタックスハイライトは不安定で信頼性が低く、(プロジェクトがある一定のサイズに達すると)テキストエディタの処理は遅くなり、リファクタリングのツールもありません。

さらに、不可解なコンパイルエラーがよく起こります。コンパイラにはまだかなりのバグが残っていて、機能的には不十分です(例えば型推論がうまくいかないことが時々あります)。

Xcodeは、私たちが使い始めて以来かなり改善されてきましたが、上に挙げた点は今も変わらず、ユーザエクスペリエンスを損なうものです。Appleがデベロッパーツールの改善にいくらかでも焦点を当ててくれることを期待しています。

数値

AppleがSwiftを発表したのは2014年のWWDCでした。同年の7月後半、私たちはSwiftだけで書いたiOSアプリとしてTest Centerの開発に着手し、11月半ばに運用を開始しました。バージョン1.0をリリースするまでアプリの開発は3カ月強かかりました(開発者は1人。Android版とWeb版は既にライブになっていたので、バックエンドと設計は既に完成し機能していました)。

前述したように堅牢さと安定性を重要と見なしていたので、その点について私たちがどのように対処したか見ていきましょう。

クラッシュ

これを書いた時点では、Test Centerはライブになってから2カ月半ほど経過していましたが、その間かなりの数がダウンロードされユーザも増えました(Appleによって取り上げられたせいもあります)。

最初のバージョンにはよくあることですが、未知の問題が出てきました。それでも
深刻なバグは見過ごさなかったようです。現在まで、Test Centerのクラッシュレートは0.2%程度で、妥当な数字だと思えます 3

もう少し詳しくクラッシュのグループ(同じ原因を持つクラッシュ)を見ていきましょう。一番多いクラッシュは(全てのクラッシュのうち30%程度)は外部のObjective-Cライブラリが原因でした。実際のところ、上位5つのうち4つまでのクラッシュの原因がObjective-C由来でした(5番目はアサーションエラーで、製品版のビルドでも有効にしていたからです)。

他にも注目すべきなのは、7番目に多かったクラッシュ原因は前述したようにAppleの提供するObjective-Cの関数に起因するものだったということでしょう。 (-[AVAssetWriterInputHelper markAsFinished])など、文書化されていないのに例外を@throwするものです。

このクラッシュの少なさはソフトウェアアーキテクチャが堅牢だったから、また私たちが健全なエンジニアリングの原則を順守したからだと思います。とはいえ、Swiftを使うことによりアーキテクチャの構築とベストプラクティスへの順応が容易になったのも確かで、これは意図的にあらゆる種類のバグがあらかじめ排除されていたからです。例えばSwiftの型システムを正しく使用することにより、コンパイル時に多くの型エラーを把握し、防ぐことが開発中にできたため、製品版になってから表面化するという事態を避けられました4

コンパイラのパフォーマンス

私たちが受けた質問として、この規模のプロジェクトでのコンパイラのパフォーマンスがどうだったかということがありました。slocで確認したところ、プロジェクトの現在のコードのラインカウントは10,634です(空行やコメントは除外しています)。
Xcodeのキャッシュを消去してtime xcodebuild -configuration Releaseを走らせるのに約2分かかります。デバッグビルドをコンパイルするのには30秒ほどです。計測は2013年中旬のMacBookPro Retinaモデルで行いました。Xibファイルのコンパイルにかかった時間も含んでいますので、Swiftのコンパイルのみの計測結果ではないということに留意してください。5
プロジェクトの規模が大きくなるにつれて、Xcodeの機能が徐々に遅くなるということは明らかです。他にも気づいた人がいるようです。反復時間(コードチェンジの後、CMD+Rの入力後、シミュレータ上でアプリを稼働させた後にかかる時間)はObjective-Cと比べて悪い結果でした。簡易なテストをしただけですが、1ライン追加すると最大14秒の待ち時間がありました。何をするかによってこの結果は大きく変化します。同様なことをObjective-Cベースのアプリケーションでやっても2~3秒しかかかりません。
さて、もちろんコンパイラのベンチマークとしてはこれは厳密なものではないので、そこは割り引いて考えてください。ただ、現状でのパフォーマンスがどの程度かという概観は得ることができるのではないかと思います。

まとめ

Objective-Cに長く携わってきた開発者たち、特にモダンなプログラミング言語に興味のある方たちにとって、Swiftは待ち望んだ心躍る一歩だと言えるでしょう。とは言え、(現在の)デベロッパツールの状況では時としてイライラしてしまうことも事実です。
Swiftを使って、(少なくとも私たちの開発したタイプのアプリケーションについては)安定し堅牢で広く使われる製品版アプリが開発できるということが証明できました。当社の主力アプリDuolingoには既にSwiftコードを一部分利用していますし、将来的にはもっと利用していくつもりです。
ではSwiftを選択する理由とは何でしょうか。最新の機種を持つユーザ層(iOS7以上をターゲットとする)を抱え、また大規模なプロジェクトを開発する耐性があるのであれば、Swiftは新鮮であり、構造のしっかりした言語なので開発に向いているでしょう。Appleが推し進めようとしているプログラム哲学を理解するためにも、ぜひ試してみることをお勧めします。
Objective-Cを完全に使いこなしているなら、Swift”だけ”を利用する方向への転換は容易で、難しいことは特にないでしょう。プログラムの書き方はObjective-Cとほぼ同じです。ただ、もう少し進んだコンセプトを取り入れようとすれば、がぜん面白くなってきます。とりわけSwiftには 関数スタイルのプログラミングを活用しようという傾向があるように見受けられますが、これは大変素晴らしいと思います。
既にObjective-Cで開発したアプリがあるなら、Swiftを使ってみるという目的だけのために書き直す必要はないでしょうが、Swiftを使って新たなコンポーネントを追加することを検討してみてもよいでしょう。
私たちがもし当時に戻ってもう一度アプリを作り直すと仮定して、再度Swiftを使うかと問われれば、答えはイエスです。
――――――――――――――――――――


  1. テスト中に正当な理由のクラッシュがあった場合は無料で再テストを受けることができます。 

  2. これよりもっといい方法があるかもしれませんが、Swift版に備わるメソッドの表現力を損ないたくなかったので、シンプルにラッパーを用いました。改善する余地はあるかもしれません。 

  3. これはダウンロード数に対するクラッシュの割合です。”起動数毎”のいいメトリックはありませんでした。アプリはダウンロード数より多く起動されているので、数値はもっと低いかもしれません。 

  4. Objective-Cが改善される(または置き換えられる)ことを私はずっと望んできました。また、私はHaskellや静的型付けの言語が気に入っています。Swiftはそちらの方向性への前進だと思います。 

  5. もしこのプロジェクトに関して何か1つ変更したいことがあるとすれば、Interface Builderを無くすことでしょう。私は、様々な理由から既存のXibを徐々にlayoutコードで置き換えています。