malcommac/Hydra コードレビュー2 (then)

概要

  • 前回HydraのPromiseについてコードを読んでいった
  • 今回はPromiseを使う上で無視できないthenを深掘りしていく

then

Hydra/Promise+Then.swift at master · malcommac/Hydra · GitHub

定義を見ていく

@discardableResult
public func then<N>(
    in context: Context? = nil, 
    _ body: @escaping ( (Value) throws -> N) ) -> Promise<N>

@discardableResult
public func then<N>(
    in context: Context? = nil, 
    _ body: @escaping ( (Value) throws -> (Promise<N>) )) -> Promise<N>

@discardableResult
public func then(
    in context: Context? = nil, 
    _ body: @escaping ( (Value) throws -> () ) ) -> Promise<Value>

上の定義を見るとthenの呼び出し方は3種類あり、以下のように捉えられる

  • 現在のPromiseを発火させた結果、解決したValueを元に
    • 指定した型の値(N)として後続のPromiseに繋げる
      • Value -> N
    • 指定した型のPromise(Promise<N>)として後続に繋げる
      • Value -> Promise<N>
    • 現在のPromiseの型(Promise<Value>)として後続に繋げる
      • Value->Void

そして@discardableResultのattributeが付いているため代入操作をしなくても

// some: Promise<Some> としたとき
some.then { _ in }

この書き方でPromiseを発火させることができる

実装を見ていく

let p = Promise<Bool> { resolve, _, _ in
    resolve(true)
}

Promise<Bool>型のインスタンスとして、ただtrueをresolveするPromise p を生成する

そしてpに対して

then (in context: Context? = nil,  _ body: @escaping ( (Value) throws -> () ) ) -> Promise<Value>

上記thenを実行する

p.then { (ret) in
    print("[then end]: \(ret)")
}

そうすると関数内部でnextPromiseという変数が生成される

let nextPromise = Promise<Value>(in: ctx, token: self.invalidationToken, { resolve, reject, operation in
    let onResolve = Observer.onResolve(ctx, { value in
        do {
            try body(value)
            resolve(value)
        } catch let error {
            reject(error)
        }
    })
    let onReject = Observer.onReject(ctx, reject)
    let onCancel = Observer.onCancel(ctx, operation.cancel)
    self.add(observers: onResolve, onReject, onCancel)
})

このnextPromiseは、

  1. bodyとして、onResolve / onReject / onCancelのObserver型のインスタンスを生成
  2. それらのObserver群はthenをcallしたPromiseのobserversに追加
  3. 元のPromise p がresolved状態になるタイミングでnextPromiseのonResolveが発動
  4. thenの引数として渡したbodyが発火しその結果resolve、もしくはrejectが呼ばれる

という動きになる、そして pnextPromise がいつ発火するのかという話になるのだが、

thenが呼ばれたタイミングでこの2つのPromiseのrunBodyを実行する

nextPromise.runBody()
self.runBody()
return nextPromise

こうすることで元の p のresolve(true)が呼ばれresolved状態からのnextPromiseの発火により

チェインしてるように見せることができる

あとは非同期で連結で実行するための定義tipsとして単純な実行、map、flatMapのように扱うために3種類のパターンで

オーバーロードで定義されているのは使い勝手としても良さそうである

public func then(
    in context: Context? = nil, 
    _ body: @escaping ( (Value) throws -> () ) ) -> Void

のように用意せず @discardableResult を使うことでシンプルなインターフェースになってると言えそう

終わりに

今回はPromiseのthenのみについて深掘りをした、自分で非同期ライブラリを実装するような場合に参考になるレビューだったように思う

次回はcatchを深掘りしていく予定