PromiseKit/swiftを読んだ

PromiseKitとは

使い方

NSURLConnection.GET("http://placekitten.com/250/250").then{ (img:UIImage) in
    // ...
    return CLGeocoder.geocode(addressString:"Mount Rushmore")
}.then { (placemark:CLPlacemark) in
    // ...
    return MKMapSnapshotter(options:opts).promise()
}.then { (snapshot:MKMapSnapshot) -> Promise<Int> in
    // ...
    let av = UIAlertView()
    // ...
    return av.promise()
}.then {
    self.title = "You tapped button #\($0)"
}.then {
    return CLLocationManager.promise()
}.catch { _ -> CLLocation in
    return CLLocation(latitude: 41.89, longitude: -87.63)
}.then { (ll:CLLocation) -> Promise<NSDictionary> in
    // ...
}.then
// ...
  • thencatchクロージャを渡してメソッドチェーンしていく。これは普通のPromiseパターンと同じ。
  • エラーが発生したら最も近いcatchで補足される。

tl;dr

  • NSURLConnection+PromiseKit.swiftのようなextensionが何種類か用意されている。
    • 拡張されたメソッドは非同期処理を開始し、Promiseオブジェクトを初期化してすぐに返す。
    • 非同期処理が成功すると、fulfillerメソッドが実行される。
  • fulfillerメソッドは以下を実行する。
    • Promiseオブジェクトのstatus.Fulfilledに更新する。
    • handlersにあるクロージャをすべて実行する。
  • Promiseオブジェクトのthenメソッドを呼ぶと以下のようなクロージャhandlersに追加され、新しいPromiseオブジェクトを返す。

NSURLConnection+Promise.swift

public class func GET(url:String) -> Promise<NSData> {
    // ...
}
  • いくつかの拡張を見てみるとすべてPromise<T>を返すようになってる。
  • この返り値に対してthencatchを呼んでいるので、これらのメソッドPromiseクラスのメソッドだと考えられる。Promiseクラスについてはあとで見ていく。
public class func GET(url:String) -> Promise<UIImage> {
    let rq = NSURLRequest(URL:NSURL(string:url))
    return promise(rq)
}
  • 冒頭の使い方のところで出てきたUIImageを扱うメソッドはこれ。
  • NSURLRequestオブジェクトを作ってpromiseメソッドというのに渡して呼んでいる。
public class func promise(rq:NSURLRequest) -> Promise<UIImage> {
    return fetch(rq) { (fulfiller, rejecter, data) in
        // ...
    }
}
func fetch<T>(var request: NSURLRequest, body: ((T) -> Void, (NSError) -> Void, NSData) -> Void) -> Promise<T> {
    // ...

    return Promise<T> { (fulfiller, rejunker) in
        // ...
    }
}
  • fetch内ではPromise<T>を初期化して返している。初期化時にまたもクロージャを渡している。
// Promise.swift

public init(_ body:(fulfiller:(T) -> Void, rejecter:(NSError) -> Void) -> Void) {
    // ...
    body(fulfiller, rejecter)
}
  • 上のようなクロージャを受け取る初期化はこれのようだ。
  • まずbodyという引数を受け取る。bodyfulfillerrejecterの2つのクロージャを受け取ってVoidを返すクロージャ(ややこしい…)である。
  • このinitでは引数として受け取ったbodyというクロージャを実行している。bodyに渡される2つの引数はinit内で定義される内部メソッドである。
// Promise.swift

public init(_ body:(fulfiller:(T) -> Void, rejecter:(NSError) -> Void) -> Void) {
    func recurse() {
        for handler in handlers { handler() }
        handlers.removeAll(keepCapacity: false)
    }
    func rejecter(err: NSError) {
        if self.pending {
            self.state = .Rejected(err)
            recurse()
        }
    }
    func fulfiller(obj: T) {
        if self.pending {
            self.state = .Fulfilled(obj)
            recurse()
        }
    }

    body(fulfiller, rejecter)
}
  • fulfillerメソッドstate.Fulfilledに変更しrecurseを呼ぶ。
  • rejecterメソッドstate.Rejectedに変更しrecurseを呼ぶ。
  • recurseメソッドは、すべてのhandlerを実行したあと消去している。
func fetch<T>(var request: NSURLRequest, body: ((T) -> Void, (NSError) -> Void, NSData) -> Void) -> Promise<T> {
    // ...

    return Promise<T> { (fulfiller, rejunker) in
        NSURLConnection.sendAsynchronousRequest(request, queue:PMKOperationQueue) { (rsp, data, err) in
            // ...

            if err {
                rejecter(err)
            } else {
                body(fulfiller, rejecter, data!)
            }
        }
    }
}
  • Promise<T>の初期化時に引数として渡されたクロージャが実行されるので、このときに非同期通信が実行されるようだ。
  • 非同期通信が成功した場合、body(fulfiller, rejecter, data!)が呼ばれる。このbodyというクロージャfetchメソッドに渡されたもので、その中のfulfillerrejecterの2つのクロージャPromise<T>init内で定義されたメソッドである。

Promise.swift

public func then<U>(onQueue q:dispatch_queue_t = dispatch_get_main_queue(), body:(T) -> U) -> Promise<U> {
}
  • シグネチャーがジェネリクスまみれで複雑。dispatch_queue_t型と(T) -> U型を引数にとり、Promise<U>型を返すメソッドということになる。
  • TはPromiseクラスの型変数(←言い方合ってる?)であり、NSURLConnection+Promise.swiftの例で言うと、このTにはNSDataNSStringが入ってくる。
  • 例えばTNSDataの場合、第2引数のbodyは「NSDataを引数にとってUを返すクロージャ」となる。このUが例えばMKPlacemarkである場合、thenPromise<MKPlacemark>を返すことになる。
  • この返り値はPromise<T>であるため再度thenを呼び出すことができメソッドチェーンが成立している。
public func then<U>(onQueue q:dispatch_queue_t = dispatch_get_main_queue(), body:(T) -> U) -> Promise<U> {
    switch state {
    case .Rejected(let error):
        // ...
    case .Fulfilled(let value):
        // ...
    case .Pending:
        // ...
    }
}
  • statePromise<T>クラスのプロパティでState<T>型として定義されている。
enum State<T> {
    case Pending
    case Fulfilled(@autoclosure () -> T)
    case Rejected(NSError)
}
public func then<U>(onQueue q:dispatch_queue_t = dispatch_get_main_queue(), body:(T) -> U) -> Promise<U> {
    switch state {
    case .Rejected(let error):
        // ...
    case .Fulfilled(let value):
        // ...
    case .Pending:
        // ...
    }
}
  • stateenum型であることが分かったので、thenに戻る。
  • このswitch文ではvalue bindingsを行っている。マッチしたcase文で宣言された変数に値が割り当てられる。例えば、.Fulfilledにマッチした場合、stateを初期化する際に.Fulfilledに渡されたクロージャvalueという変数に割り当てられる。
public func then<U>(onQueue q:dispatch_queue_t = dispatch_get_main_queue(), body:(T) -> U) -> Promise<U> {
    switch state {
    // ...
    case .Pending:
        return Promise<U>{ (fulfiller, rejecter) in
            // ...
        }
    }
}
  • statusは宣言時に初期値として.Pendingを渡しているため、最初は.Pendingのcase文を通ることになりそう。
  • status.Pendingである場合、Promise<U>を初期化して返している。
  • 初期化の際、引数にクロージャを渡している。上述の通り、渡されたクロージャは初期化処理の最後に実行される。
public func then<U>(onQueue q:dispatch_queue_t = dispatch_get_main_queue(), body:(T) -> U) -> Promise<U> {
    switch state {
    // ...
    case .Pending:
        return Promise<U>{ (fulfiller, rejecter) in
            self.handlers.append{
                switch self.state {
                case .Fulfilled(let value):
                    fulfiller(value())
                case .Rejected(let error):
                    dispatch_async(onQueue){ fulfiller(body(error)) }
                case .Pending:
                    abort()
                }
            }
        }
    }
}
  • Promise<U>の初期化の最後でself.handlersクロージャが追加されている。上述の通り、handlersfulfillerrejecter内で呼ばれるrecurseですべて実行される。
  • つまり、then()に渡されたクロージャhandlersに追加され、そのPromiseオブジェクトの非同期処理が完了したときに呼ばれることになる。

バックエンドAPI用のテンプレートを作り始めた

モバイルアプリケーションやJavaScriptアプリケーションのバックエンドとして使うAPIのテンプレートを作り始めた。

https://github.com/naoty/metallic

まだそんなにできてないけど、連休終わるので進捗を書いておく。まだ公開できるレベルではないので公開はしてない。

動機

iOSアプリのバックエンドをサクッと作りたい、でも単なるデータストアとしてではなくて少しロジックを実装したい、ってときにMBaaSを使うよりも自分でサーバーサイドを実装したくなる。そのとき、RailsSinatraかを選ぶことになる。Railsでももちろん問題ないのだけど、必要十分な処理さえしてくれればいいという思いからSinatraを選びたくなる。だけど、Sinatra単独でバックエンドを実装するのにはかなり時間がかかる。例えば、データベースとの接続やマイグレーションの管理、JSONのパースと出力などなど、地味に大変な実装をこなさないといけない。そこで、SinatraベースでバックエンドAPIを実装するためのテンプレートを作ることにした。

現時点での機能

  • metallic new APPLICATION_NAME: テンプレートからプロジェクト作成する。
  • metallic generate controller RESOURCE_NAME: テンプレートからコントローラーを作成する。コントローラーはSinatra::Baseを継承したRESTful APIを持つクラス。作成されたコントローラーは自動的にRackミドルウェアとしてuseされる。
  • metallic generate model RESOURCE_NAME: テンプレートからモデルとマイグレーションを作成する。今のところORMはActiveRecord固定で、DBもSQLite3固定になってる。ここはRailsみたいにオプションで切り替えられるようにしたい。Rakefileもテンプレートについてくるので、そのままrake db:migrateできる。

使用例

$ metallic new todo
      create  todo/Gemfile
      create  todo/Rakefile
      create  todo/app/application.rb
      create  todo/config.ru
      create  todo/config/database.yml
$ cd todo
$ bundle install
$ metallic generate controller tasks
      create  app/controllers/tasks_controller.rb
$ metallic generate model Task
      create  app/models/task.rb
      create  db/migrations/20140721224324_create_tasks.rb
$ rake db:migrate
== 20140721224324 CreateTasks: migrating ======================================
-- create_table(:tasks)
   -> 0.0011s
== 20140721224324 CreateTasks: migrated (0.0012s) =============================
$ rackup
[2014-07-21 22:43:34] INFO  WEBrick 1.3.1
[2014-07-21 22:43:34] INFO  ruby 2.1.2 (2014-05-08) [x86_64-darwin13.0]
[2014-07-21 22:43:34] INFO  WEBrick::HTTPServer#start: pid=20212 port=9292
$ curl http://localhost:9292/tasks
GET /tasks

実装予定

  • 各種必要なRackミドルウェア: bodyのJSONをパースするヤツ、パラメータをパースしてヘルパーからページ番号やソートにアクセスできるようにするヤツ、例外をキャッチして適切なステータスコードを返すヤツ、整形されたJSONを返すヤツ、などをRackミドルウェアとして実装したい。
  • 他DB対応: generateコマンドのオプションでテンプレートを切り替えられるようにしたい。
  • あと、方針を決めかねているけど、Railsと組み合わせて使えるようにしたい。というのも、モバイルアプリケーションのバックエンドとしてだけじゃなくてJavaScriptアプリケーションのバックエンドとしても使えるようなものを目指しているので、部分的にmetallicアプリケーションをRailsアプリケーションにマウントさせる、みたいな使い方も考えられそう。なので、そういう場合を想定したテンプレートを考えたい。

APNsの概要と関連ツール群

Apple Push Notification Service(APNs)は、ソフトウェア開発者(プロバイダ)から受け取ったメッセージを安全な方法でデバイスにプッシュ通知するサービスである。

プッシュ通知までの流れ

  1. プロバイダはデバイストークンとペイロードから成る通知メッセージを作る。
  2. プロバイダはその通知メッセージをAPNsに送信する。
  3. APNsは受け取った通知メッセージのデバイストークンから配信先のデバイスを特定し、通知メッセージを配信する。

接続を確立するまでの流れ

  1. APNsとデバイス間で認証を行う(システムによって行われるため、開発者が実装する必要はない)。
  2. APNsとプロバイダ間で認証を行う。
    1. プロバイダがAPNsからサーバ証明書を取得し、検証する。
    2. プロバイダがプロバイダ証明書をAPNsに送信する。
  3. デバイストークンを生成しプロバイダと共有する。
    1. アプリケーションがリモート通知の登録を行う。
    2. システムがリモート通知の設定を行い、デバイストークンをアプリケーションデリゲートに渡す。
    3. アプリケーションがデバイストークンをプロバイダに送信する。
  4. プロバイダからの通信すべてにデバイストークンを添付させる。

APNs関連ツール

AFNetworkingでおなじみのmatttさんが様々な関連ツールを開発している。それぞれのツールが上で説明した全体像の中でどのような位置づけなのか整理した。

houston

https://github.com/nomad/houston

プロバイダからAPNsに向けて通知メッセージを送るためのクライアント。Ruby製。上記の通り、プロバイダがAPNsにメッセージを送るには、(1)APNsとの間で認証を行う、(2)配信先のデバイストークンを取得する必要がある。なので、houstonでもメッセージを送る際にAPNsのサーバー証明書と配信先のデバイストークンを設定する必要がある。

rack-push-notification

https://github.com/mattt/rack-push-notification

iOSアプリからデバイストークンを受け取りDBに保存するRackアプリケーション(Sinatraベース)。上述の通り、iOSアプリはAPNsからデバイストークンを取得したあと、プロバイダと共有する必要がある。rack-push-notificationはそのデバイストークンを受け取るためのAPIを用意する。

Orbiter

https://github.com/mattt/Orbiter

iOSアプリからデバイストークンを送信するためのクライアント。今までの2つのツールはプロバイダ側のツールだったが、OrbiterはiOSアプリ側のツールである。取得したデバイストークンをプロバイダに送信する処理を簡略化できるようだ。

まとめ

これらの関連ツールを使ったプッシュ通知のフローは以下のようになる。

  1. iOSアプリはリモート通知の登録を行い、APNsからデバイストークンを取得する。
  2. iOSアプリはOrbiterを使ってデバイストークンをプロバイダに送信する。
  3. プロバイダはrack-push-notificationを使って用意したAPIからデバイストークンを受け取りDBに保存する。
  4. プロバイダはhoustonを使ってプッシュ通知をAPNsに送信する。
  5. APNsはプロバイダ証明書とデバイストークンからプッシュ通知を転送するデバイスを特定しプッシュ通知を送る。

参考

pecoでハッカーを検索

かなり前にcui-about.meというサービスを作ったんだけど、pecoと相性がいいことに気づいたので組み合わせてみた。

f:id:naoty_k:20140704175901g:plain

about() {
    if [ $# -eq 0 ]; then
        local name=$(curl -s cui-about.me/users | peco)
    else
        local name=$1
    fi
    curl cui-about.me/$name
}

技術メモの管理

今後ちゃんと学んだことをメモに残しておこうと思い直し、メモを管理する仕組みを整理した。

保存場所

Dropbox/Documents/notes/以下。複数のPC間で簡単に共有したいのでDropboxで管理する。

メモを書く

まず以下のような関数を用意した。

note() {
    local note_path=$HOME/Dropbox/Documents/notes/$1
    if [ ! -e $note_path ]; then
        touch $note_path
    fi
    open $note_path
}

note <ファイル名>でメモを書き始めることができる。

メモはすべてmarkdown形式で、エディタはvimを使うことにした。vim-templateというプラグインを使うことで、notes/*.mdにマッチするファイルを以下のようなテンプレートで開くようにした。

---
title: <%= expand('%:t:r') %>
date: <%= strftime('%Y-%m-%d') %>
---

<+CURSOR+>

<%=%>で囲われたコードはVim Scriptとして評価されて展開され、<+CURSOR+>の位置をカーソルの初期位置としてファイルが開く。このような設定はvim-templateのヘルプにあるのでそちらを参照してほしい。

メモの検索

agとpecoを使う。agは高速なgrepということで、任意の文字列を含むファイルを検索する。

onote() {
    echo $(ag -l $1 $HOME/Dropbox/Documents/notes/) | peco | xargs open
}

template内で日付を必ず入れるようにしているので、日付で検索することもできるようになった。

ChefでRaspberry Piをセットアップする

仕事で複数台のRaspberry Piをセットアップすることになったので、Chefを使ってセットアップを自動化することにした。Chef、Vagrant、Serverspecなどいろいろな周辺ツールの全体像を整理したり、それらを使ったワークフローを体験できてよかったので、ブログとして残しておく。

また、セットアップに使ったChefのレポジトリはgithubにホストしてあるので参考にどうぞ。

https://github.com/naoty/chef-repo

今回、Chefで自動化したのは以下の通り。

  • apt-getの更新
  • gitのインストール
  • rbenvを使ってRuby 2.0.0-p247をインストール
  • nodebrewを使って最新安定版のnode.jsをインストール
  • Wiringpi(GPIOを簡単に操作するためのライブラリ)のインストール
  • mjpg-streamer(Webカメラを使ったストリーミングのためのライブラリ)のインストール

1. Vagrantで仮想環境を用意する

いきなりRaspberry PiにChefを使って環境構築を行うのは失敗したときにやり直すのが大変。なので、Raspberry Piに近い仮想環境を用意して、そこでChefを使ったセットアップを試行錯誤したい。そういうときに便利なのがVagrant。Vagrantを使えば簡単に仮想環境を作ったり壊したりできるので、失敗してもすぐにやり直せる。

今回、重要だったのがRaspberry Piに近い仮想環境を用意することだった。Vagrantにはboxという仕組みがあって、CentOSとかUbuntuとかいろんなOS、CPUに合わせたひな形がたくさん用意されている。通常はここにあるboxを使えばいいんだろうけど、Raspberry Piに近いboxがなかった。Raspberry Piに近いboxを探したところ、これがよさそうだったので使うことにした。

$ git clone https://github.com/nickhutchinson/raspberry-devbox raspberry_pi
$ cd rasbperry_pi
$ vagrant up

以上、これだけでRaspberry Piに近い仮想環境を用意することができた。

2. Chefのセットアップ

ここはいろんなところで解説されてる通りに行っただけ。

$ vagrant ssh-config --host vm-raspberry_pi >> ~/.ssh/config
$ knife solo init chef-repo
$ knife solo prepare vm-raspberry_pi

3. クックブックの作成とテスト

ここから環境構築の手順をコードとして記述していく。クックブックの書き方については「入門ChefSolo」やOpscodeの公式ドキュメントを参考にした。このときの注意点としては、Raspberry PiはRubyやnode.jsのインストールに非常に時間がかかるため、timeoutをとても長くする必要がある。数時間はかかると考えた方がいい。

書いたクックブックを実行する前にVagrantをサンドボックスモードにしておく。こうすると、失敗したときに実行した部分だけやり直すこと(ロールバック)ができる。サンドボックスモードにするためにはsaharaというVagrantのプラグインが必要なのでインストールしておく。

サンドボックスモードをオンにしてクックブックを実行したあと、本当に期待した通りに環境構築できたかどうかをServerspecを使ってテストする。Serverspecにはいくつかテストを実行する方法があるようだけど、今回はSSHでログインしてテストを実行する形式を採った。テストを通らなかった場合は、saharaを使ってロールバックしてやり直す。テストが通った場合は、saharaを使って変更を確定させる(コミット)。

これをサイクルさせながら、どんどんクックブックを追加していく。以上をコマンドで表すとこんな感じ。

$ knife cookbook create ruby -o site-cookbooks
$ vi site-cookbooks/ruby/recipes/default.rb
$ vi nodes/vm-raspberry_pi.json
$ vi spec/vm-raspberry_pi/ruby_spec.rb
$ vagrant sandbox on
$ knife solo cook vm-raspberry_pi
$ rspec
$ vagrant sandbox commit

4. Raspberry PiをChefで環境構築する

仮想環境での環境構築が完了したら、いよいよ本物のRaspberry Piにクックブックを適用する。そのためにはnodes以下に本物用の設定を追加するだけでいい。

$ vi nodes/raspberry_pi.json
$ knife solo cook raspberry_pi