PromiseKit/swiftを読んだ

PromiseKitとは

  • http://promisekit.org/
  • iOSプログラミングで頻繁に出てくる非同期処理を簡単かつエレガントにするライブラリ。
  • JavaScriptとかでおなじみのPromiseパターンの実装と、各種CocoaフレームワークからPromiseを使うための拡張が含まれている。
  • Objective-C版とSwift版がある。

使い方

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オブジェクトを返す。
    • thenメソッドの引数のクロージャを実行する。
    • その返り値をfulfillerに渡して実行する。

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
        // ...
    }
}
  • 引数に渡したNSURLRequestオブジェクトをfetchメソッドに渡して呼び出している。
  • fetchメソッドはさらにクロージャを受け取っている。
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)
}
  • Fulfilledは引数に() -> T型のクロージャをとる。@autoclosureは指定された引数を暗黙的にクロージャとして扱えるようにする。これによって引数を{ ... }で囲う必要がなくなる。cf) https://developer.apple.com/swift/blog/?id=4
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:
        // ...
    }
}
  • stateはenum型であることが分かったので、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オブジェクトの非同期処理が完了したときに呼ばれることになる。