読者です 読者をやめる 読者になる 読者になる

FriendlyErrorType

Objective-C/Swift/iOS

新しいエラーハンドリング

Swift 2でthrowを使ったエラーハンドリングが新たに導入された。従来のNSErrorを使ったエラーハンドリングの問題点は、メソッドNSErrorポインタの代わりにnilを渡すことで無視できてしまうことだった。新たに導入されたエラーハンドリングでは、throwsキーワードが宣言されたメソッドを呼び出す際にdo-catch文で囲うことを強制される。throwで投げられるエラーはNSErrorではなくErrorTypeというprotocolを実装した値だ。Cocoaフレームワーク内のNSErrorを使っていたメソッドthrowsを使うように置き換えられており、今後は独自のエラーを定義する場合はNSErrorではなくErrorTypeを使うのが望ましいと考えられる。しかし、ErrorTypeにも問題点はあり現実的な設計方針を検討する必要がある。

アプリ独自エラーの実装

NSErrorの代わりにErrorTypeを使っていく流れがあるものの、ErrorTypeにはNSErrorが持っていたlocalizedDescriptionuserInfoといったエラー情報がないという問題点がある。そこで、ErrorTypeを継承した新たなprotocolを定義するという方針を考えてみた。

protocol FriendlyErrorType: ErrorType {
    var summary: String { get }
    var reason: String? { get }
    var suggestion: String? { get }
}

このFriendlyErrorTypeを使って以下のように独自エラーを定義できる。

enum ApplicationError: FriendlyErrorType {
    case SomethingWrong
    case DecodeFailed([String])
    
    var summary: String {
        switch self {
        case .SomethingWrong:
            return "Something wrong"
        case .DecodeFailed(_):
            return "Decode failed"
        }
    }
    
    var reason: String? {
        switch self {
        case .SomethingWrong:
            return .None
        case .DecodeFailed(let fields):
            let failedFields = fields.joinWithSeparator(", ")
            return "Failed to decode following fields: \(failedFields)"
        }
    }
    
    var suggestion: String? {
        switch self {
        case .SomethingWrong:
            return .None
        case .DecodeFailed:
            return .None
        }
    }
}

また、CocoaフレームワークメソッドErrorTypeを投げるようになったものの、Alamofire等のライブラリを使う際にはNSErrorを使うことになるため、FriendlyErrorTypeを実装するようにNSErrorを拡張する。

extension NSError: FriendlyErrorType {
    var summary: String {
        return localizedDescription
    }
    
    var reason: String? {
        return userInfo[NSLocalizedFailureReasonErrorKey] as? String
    }
    
    var suggestion: String? {
        return userInfo[NSLocalizedRecoverySuggestionErrorKey] as? String
    }
}

なぜprotocol extensionではなく継承なのか

protocol extensionだとErrorTypeにデフォルトの実装を与えることになる。その場合、ErrorTypeとして渡されたエラーに対してメソッドを呼ぶと、すべてそのデフォルトの実装の結果が返るようになる。一方、FriendlyErrorTypeはただのprotocolなので、メソッドの結果はメソッドを実装する各クラスの結果を反映する。

extension ErrorType {
    var summary: String {
        return ""
    }
}

extension NSError {
    var summary: String {
        return localizedDescription
    }
}

let error: ErrorType = NSError(domain: "com.github.naoty.playground", code: 1000, userInfo: [NSLocalizedDescriptionKey: "Something wrong"])
print(error.summary) //=> "\n"
protocol FriendlyErrorType: ErrorType {
    var summary: String { get }
}

extension NSError: FriendlyErrorType {
    var summary: String {
        return localizedDescription
    }
}

let error: FriendlyErrorType = NSError(domain: "com.github.naoty.playground", code: 1000, userInfo: [NSLocalizedDescriptionKey: "Something wrong"])
print(error.summary) //=> "Something wrong\n"

エラーの利用例

FriendlyErrorTypeを実装したエラー型を実際に利用してみる。Alamofire、SwiftTask、Himotokiを使ってQiita APIを呼び出している。

return Task<Void, [Item], FriendlyErrorType> { progress, fulfill, reject, configure in
    Alamofire.request(.GET, "https://qiita.com/api/v2/items").responseJSON { response in
        switch response.status {
        case .Success(let value):
            if let objects = value as? [AnyObject] {
                var items: [Item] = []
                for object in objects {
                    do {
                        let item = try decode(object) as Item
                        items.append(item)
                    } catch DecodeError.MissingKeyPath(let keyPath) {
                        reject(ApplicationError.DecodeFailed(keyPath.components))
                    } catch {
                        reject(ApplicationError.SomethingWrong)
                    }
                }
                fulfill(items)
            } else {
                reject(ApplicationError.DecodeFailed(["root"]))
            }
        case .Failure(let error):
            reject(error)
        }
    }
}

NSErrorを拡張しているため、ApplicationErrorNSErrorFriendlyErrorTypeとして並べて扱うことができている。

FriendlyErrorTypeを使ってアラートを表示する実装は以下のようなイメージだ。

let title = error.summary

var message = ""
if let reason = error.reason {
    message += reason
    message += "\n"
}
if let suggestion = error.suggestion {
    message += suggestion
}

let alertController = UIAlertController(title: title, message: message, preferredStyle: .Alert)
presentViewController(alertController, animated: true, completion: nil)

以上のような方針に基づいたサンプルアプリケーションを用意した。

2015年の振り返りと2016年に向けて

雑記

2015年

2015年はRailsをやってる時間が長かった。Androidを少々書いていた時期もあり、ほんの僅かな時期iOSObjective-C)をちょろっとやってた。Rails自体への意識を高めていこうとした時期もあり、その結果として初のrails本体へのコミットも果たした。ただ、コードを書くこと自体よりも、チーム全体の成長や開発効率を高めることに集中していたように思う。どれか一つに注力していたわけではなかったので、外部イベントで話せるような深い話がまったくできなかったのが歯がゆかった。

関数型プログラミング、DDD、Go言語など新しい分野の勉強もしていた。特にGo言語は、自分用の小さいツールをたくさんGoで書いたことでそれなりに書けるようになってきたと思う。一番驚いたのは、naoty/Timepieceがなぜかバズって、海外からStarをたくさんいただいたことだった。

開発以外のトピックとしては、7、8年かけてたメガネをやめてコンタクトにしたこと、卓球ハウスが解散して練馬区に引っ越したこと、いろいろあって"社会的状況"になりつつあることがある。

開発以外の時間が充実しすぎてあんまり開発してなかったが、最高の一年となった。開発してても幸せにはなれない、そこに幸せはない、という数年来の想いが確信へと変わった一年だった。

2016年

今年こそはiOSアプリ開発に専念できたらしたい。Railsはいい加減コリゴリなので。アプリ開発に専念するからには、UI設計などのデザイン業務にも徐々に慣れていきたいなーと欲が出てきている。

前述のTimepieceの保守も継続させて☆1000を突破したい。ただ、これ以外にも他のジャンルで新しいiOS開発関連ライブラリを作っていきたい。できれば、少し大きめのイベントでも発表できるようになっていきたい(これまではあまり外部での発表に重きを置いてなかったけど、なんだかいろいろ心配になってきたので)。

あとは、12月の引っ越しを期に人生初の自炊生活を始めたので、ちゃんと自炊を習慣にしていきたいというのが大きな目標。

本年も何卒宜しくお願い致します。

「エンジニアのための時間管理術」を読んだ

書評

エンジニアのための時間管理術

エンジニアのための時間管理術

この本を読もうと思った背景は2つある。1つは、前回のエントリーでも書いた通り、最近は仕事を効率的に終わらせてプライベートの時間をちゃんと確保しようとしていることだ。もう1つは、7月から新卒のメンターを担当しており、作業の割り込みが増えたことだ。新人には分からないことがあればどんどん割り込んでほしいと伝えているため、メンターが割り込みを処理しつつ普段の業務もそつなくこなす必要がある。

この本は「エンジニアのための」と書いてあるが実際はシステム管理者をターゲットとしており、プログラマーである僕とは少し事情が異なるかもしれない。それでも、割り込みへの対処法やタイムマネジメントなど、その他の職業にとっても有用なノウハウがたくさん書いてある。

本書を通して感じたことは「考えることを減らして、目の前の作業に集中しよう」というメッセージだ。良い習慣を身につけたり、外部記憶装置やカレンダーのようなツールを使ったり、単純で頻繁に行う作業を自動化したりすることで、考えることを減らすことができ、割り込みやタイトスケジュールによって作業効率が落ちるのを避けられるという話が多かったように思う。

その中で特に印象に残ったのは、とにかく外部記憶装置を多用しようという話だ。本書ではPDAがその例として出てくるが、若者はPDAなんて使わないと思うので、仕事でよく使うツールに置き換えて読んでいた。本書で言われていた通り、頭のなかで覚えておく作業が増えてくるとだんだん目の前の作業に集中できなくなってくる気がする。

僕の場合、会社ではメール、スケジュールはすべてOutlookで管理されているので、やるべき作業もすべてOutlookで管理するようにしてみた。その日のTODOリストは自作のtodo管理ツール*1で管理しているが、1日の始まりにOutlookからやるべきことを棚卸ししてTODOリストに追加している。逆に、すべてのTODOをTODOリストで管理してしまうと、TODOリストがいつになっても空にならず達成感がもてなくなってしまう。すべてのタスクを記録する外部記憶装置と短いTODOリストに分けることでうまく機能しそうだ。

その他にも今すぐ実践できそうなノウハウがたくさんあったので試行錯誤して効率的に仕事をこなせるようになりたい。

近況

雑記

最近大きな環境の変化があったり先日行ってきたYAPCで影響を受けて心境が変わってきたので、近況という形でブログに残しておきたい。

大きな心境の変化として、プライベートの時間の使い方を変えようと思っている。より優先度の高いことに時間を使うようにして、プライベートでプログラミングする時間は以前より少なくなりそう。あれこれ手を出すというよりは1つのことに集中して時間を充てる方が効果的なんじゃないかと思うようになったので、何にしようか考えている。

  • 仕事で使う可能性がありそうなScalaとPlayFrameworkに興味がある。以前にすごいH本を読んで関数型プログラミングを実践してみたい気持ちがある。
  • あとは、いま仕事ではRailsプロジェクトに携わっているけど、リファクタリングを喫緊の課題として感じていて、プライベートの時間でいかに対処すべきか本を読んで考えてみたり、必要なライブラリの開発に時間を充てるのもいいかもしれない。
  • JavaScriptをキャッチアップしたい気持ちもちょっとある。YAPCのセッションでES6の話を聞いて、ちゃんと学んで既存のプロジェクトに手を加えたい気持ちが湧いてきた。ただこれは上のリファクタリングが済んだ先の話だと思った。
  • ドメイン駆動設計」をずっと読んでたけど、引き続き「実践ドメイン駆動設計」も読んでみようかなという選択肢もある。ただ、まだ仕事で使えるような段階にはない気がする。
  • naoty/Timepieceの開発は停滞しているけど、どうしようかなと思っている。仕事でSwiftが書けるのであればまだモチベーションを維持できるのだけど、残念ながらそのような環境にはいない。

こう整理してみると、リファクタリングについてプライベートでも時間をとるのがよさそうだなと思う。それが一段落ついたらJavaScriptリファクタリングとかDDDとかに移っていけそう。もっと余裕が出てきたらScalaやろうかなぁ。

なんとなくあと2, 3年先までの見通しが立ってきた。仕事ではとりあえず今のプロジェクトを地道に改善していくことになりそう。自分の成長というよりはプロダクトの成功やチームの成長にフォーカスしていきたいという気持ちに移ってきている。Rubyの上にも3年、という感じ。iOS/Androidもやるけど。プライベートでも、今の生活の先に明るい未来が見えてきている。堅実に仕事を進めつつ、貴重な時間を大切にしていきたい。

Timepieceを0.4.0にアップデートした

Objective-C/Swift/iOS

Timepieceを0.4.0にアップデートした。ぶっちゃけブログの記事にするくらいならちゃんとCHANGELOGにしろという話なんだけど、技術的な詳細も少し話したいのでブログの記事にした。軽微なバグの修正と以下の2点が今回の変更点だ。

タイムゾーンのサポート

これがメインの変更になる。「サーバー側から受け取る時間のタイムゾーンiOSアプリケーションのタイムゾーンが異なっており、それらを比較したい」みたいなissueをもらったので対応した。「こういう感じのインターフェイスはどう?」みたいなのを聞いてたら「Sweet!」だのと褒められたので、その気になって実装してしまった。けっこう大変だった。

iOSでは、NSDateオブジェクトそのものにタイムゾーンは存在しない。システムで設定されるタイムゾーンNSCalendar経由で取得することになる。

NSCalendar.currentCalendar().timeZone

なので、今回のようなケースだと、NSDateオブジェクトそれぞれにタイムゾーンが存在するように見せる必要がありそうだった。なお、オフセットを調整することも考えられるが、時間を足し引きしてしまった段階でそれは別の時間となってしまう。同じ時間で別のタイムゾーンを持つNSDateオブジェクトが必要だった。実装方針としては、

ということを念頭に置いた。

ところで、TimepieceはNSDateを始めとするいくつかの既存のオブジェクトのextensionとして実装されている。extensionでNSDateオブジェクトにtimeZoneのようなプロパティを追加することは普通はできない。しかし、Objective-CのランタイムAPIを使って動的にプロパティを追加することで、これをなんとか実現させることができる。Swiftの場合でも、ランタイムAPIを使うことは可能だ。

import ObjectiveC

public extension NSDate {
    var timeZone {
        return objc_getAssociatedObject(self, &AssociatedKeys.TimeZone) as? NSTimeZone ?? calendar.timeZone
    }

    func change(#timeZone: NSTimeZone) -> NSDate! {
        // ...

        objc_setAssociatedObject(newDate, &AssociatedKeys.TimeZone, timeZone, UInt(OBJC_ASSOCIATION_RETAIN_NONATOMIC))

        // ...
    }
}

objc_から始まる関数がランタイムAPIだ。これらの関数によって動的にオブジェクトにプロパティを読み書きしている。change(timeZone:)で生成されたNSDateオブジェクトのみ動的に追加されたプロパティを持つ。タイムゾーンが異なるNSDate同士の計算も問題なく行われている。これは上述の通りオフセットの調整による実装ではないためだと思う。

playgroundの追加

機能というわけじゃないけど、リポジトリにplaygroundを追加した。それに伴ってプロジェクトからワークスペースに変更した。実際にTimepieceを試してもらうには、サンプルのアプリケーションを作ってPodfileを書いて…みたいなことをする必要がありとても面倒だったので、Timepieceが使えるplaygroundを用意した。

Pull request大歓迎

タイムゾーン周りの実装は正直あんまり自信はないんだけど、テストはちゃんと通ってるしまぁいいかくらいの気持ちでリリースした。観点が漏れている可能性は大いにありうるので、ぜひPull requestしてもらいたい。

コマンドラインを拡張しやすくするヤツ書いた

Go

gitなど既存のコマンドラインを拡張して新しいサブコマンドを追加する方法はいくつか考えられる。

git alias

gitの場合はgit aliasを使うことで簡単にサブコマンドを追加できる。gitのとき限定。

ラッパー

github/hubのような既存のコマンドラインをラップしたスクリプトを書き、alias hub=gitのようにaliasすることで既存の機能を保ちつつ機能を追加できる。

問題点としては、複数のラッパーによる拡張が難しくなる。例えば、ここでbubというgitのラッパーを書いたとする。githubの機能とbubの機能を拡張したい。hubは入力されたサブコマンドがhubになければgitにフォワードしている。なので、hubbubを同時に拡張するにはbubhubのラッパーとして実装することになってしまう。依存関係をハードコーディングすることになるため、まったくスケーラブルじゃない。

命名規則とext

command subcommandと入力されたらcommand-subcommandを実行するように名前解決する仕組みがよさそうだと思う。例えば、git prというコマンドはまずgit-prを探し、あれば実行し、なければgit prを実行する(そしてエラーになる)。gem uninstall allというコマンドはgem-uninstall-allgem-uninstall allgem uninstall allの順に探索されて見つかり次第実行される。

このような命名規則を基に名前解決するツールを書いた。

$ go get github.com/naoty/ext
$ go get github.com/naoty/gem-uninstall-all
$ alias gem="ext gem"
$ gem uninstall all # Run gem-uninstall-all

正直、いろんな問題がありえそうだが、昨日思いついたままに書いたものなので、まだ想定できてない。gem-uninstall-railsというコマンドがあったらrailsをアンインストールできないとかありそう。

上の例で、hubbubを同時に拡張したい場合にextを使うと以下のようにできる。

$ go get github.com/naoty/hub-bub
$ alias git="ext hub"
$ git bub # Run `hub-bub`

残念ながら、hubを使いたい場合はこうするしかないような気がする。


追記(2015-07-23)

gitには、git subcommandgit-<subcommand>として名前解決して実行する機能があったことをさっき知った。なので、gitに限って言えばextのようなツールは不要だと思う。

$ cd $HOME/bin
$ vi git-hello
#!/bin/sh

echo "Hello, world!"
$ chmod +x git-hello
$ git hello
Hello, world!

Pocketのもう読んでない記事を掃除するヤツ書いた

Go

普段、Pocketを使って「あとで読む」記事を管理している。ちょっとした時間に見つけた記事をPocketに追加しておいて、通勤時間などにiOSアプリで消化している。ただ、長い記事が増えてくるとだんだん消化しきれなくなってきて、消化しようというモチベーションが失せてくる。そこで、一定期間が経っても消化できていない記事を自動的に削除するツールを書いた。

使い方

  1. まず、Pocketのdeveloperサイトに行ってアプリケーションを作成する。すると、Consumer keyが得られる。次に、Access tokenが必要なのだけど、これはmotemen/go-pocketを使ってOAuth認証を行い取得した。

  2. Herokuボタンからデプロイする。ここで、上で取得したconsumer keyとaccess tokenを環境変数としてセットする。さらに、削除対象とする期限を環境変数で指定できる。デフォルトは24時間となっている。僕は72時間にしている。

  3. Heroku schedulerのダッシュボードでsweepを実行するタイミングを設定する。

Deploy

所感

先日、Herokuが公式にGoをサポートしたので、さっそくテストを兼ねてこういうものをGoで書いてみた。tools/godepの使い方を覚えなくてはいけないことを除けば、いつもどおりにHerokuにアプリケーションをデプロイできた。

個人的にちょっとしたCLIツールをGoで書くことが増えたが、ちょっとしたジョブを定期実行させるときにGoでちょっとしたツールを書いてHeroku schedulerにやらせるという手法は非常にお手軽なので今後も機会がありそうだなと思った。