malcommac/Hydra コードレビュー1 (Promise)

概要

github.com

  • 軽量なPromiseライブラリ
  • Async&Awaitもサポートしてる
  • 便利な独自オペレータも豊富で使い勝手が良い

導入

軽量を謳ってるだけあって依存してるライブラリもなくシンプルにHydraAsyncのみの導入だけで良い

cocoapods

pod 'HydraAsync'

carthage

github 'HydraAsync'

定義を見ていく

まずは肝となるPromise.swiftから実装を見ていく

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

クラス定義

返却される型をValueの型変数として指定する

public class Promise<Value>

typealias

Resolved

public typealias Resolved = (Value) -> ()

処理が解決した場合に使用される型定義、型変数として指定したValueが返ってくる

Rejector

public typealias Rejector = (Error) -> ()

処理が失敗した場合、もしくはBodyのスコープ内で例外をthrowした場合に使用される型定義

Body

public typealias Body = (
    (_ resolve: @escaping Resolved,
    _ reject: @escaping Rejector,
    _ promise: PromiseStatus) throws -> ()
)

解決、失敗、キャンセルの操作をまとめて扱える型定義

イニシャライザ

通常

public init(
    in context: Context? = nil, 
    token: InvalidationToken? = nil, 
    _ body: @escaping Body
)

指定したcontext上でどういう操作をするかを指定した上でPromiseの初期化を行う

即時解決

public init(resolved value: Value)

指定したValueで処理が解決した状態でPromiseの初期化を行う

即時失敗

public init(rejected error: Error)

指定したエラーの内容で処理が失敗した状態でPromiseの初期化を行う

実装を見ていく

定義を見たところよくあるPromiseの定義にキャンセル操作が付いただけなのでシンプルさがある

そこで具体的に内部の実装でどうやってPromiseを実現しているのかを掘っていってみる

Promiseの状態管理

まずHydraの内部として大まかにinternalなPromiseの状態が2種類ある、

  • state: State
    • pending
      • まだ操作をしてない状態
    • resolved(_: Value)
      • 解決した状態
    • rejected(_: Error)
      • 失敗した状態
    • cancelled
      • PromiseStatusを通してキャンセルの動作を行った状態
  • bodyCalled: Bool
    • イニシャライザとしてbodyを渡す場合にbodyが実行されたかどうか
    • 重複で呼び出されないように状態として保持している

これらを駆使して状態管理を実現している、そして状態を変化させる場合はqueueを通しており、

(例外としてキャンセルの操作のみ処理が走ってる場合でも割り込める必要があるのでqueueは通していない)

Promiseの状態変化専用のqueueとしてのDispatchQueueであるstateQueueが定義されている

/// This is the queue used to ensure thread safety on Promise's `state`.
internal let stateQueue = DispatchQueue(label: "com.mokasw.promise")

ドキュメントコメントにもある通りこのqueueがあるおかげでスレッドセーフにPromiseが扱えるということになる

Promiseの実行

HydraのPromiseは実行する際のフローとして最終的にPromise.swift内に定義されているrunBodyが呼ばれる

もちろんライブラリ使用者から呼ばれないようにするためにinternalなアクセスとなっていて、

基本的にはHydraが定義しているオペレータ群から叩かれることになる

internal func runBody() {
    self.stateQueue.sync {
        if !state.isPending || bodyCalled {
            return
        }
        bodyCalled = true
        
        // execute the body into given context's gcd queue
        self.context.queue.async {
            do {
                // body can throws and fail. throwing a promise's body is equal to
                // reject it with the same error.
                try self.body?( { value in
                    self.set(state: .resolved(value)) // resolved
                }, { err in
                    self.set(state: .rejected(err)) // rejected
                }, self.operation)
            } catch let err {
                self.set(state: .rejected(err)) // rejected (using throw)
            }
        }
    }
}

まずは既に実行されてるかどうかを上述で説明したPromiseの状態を見て確認し、

その後に実際の実行処理に入りそれぞれBodyのclosure内で呼ばれた操作に従い

Promiseの状態をpendingから他の状態への遷移を決定している

(もちろんPromiseの仕様通りで、pending以外の状態から別の状態に切り替わることはない)

private func set(state newState: State) {
    self.stateQueue.sync {
        // a promise state can be changed only if the current state is pending
        // once resolved or rejected state cannot be change further.
        guard self.state.isPending else {
            return
        }
        self.state = newState // change state
        self.observers.forEach { observer in
            observer.call(self.state)
        }
        self.observers.removeAll()
    }
}

observer.callによりPromiseを呼び出した際に指定したclosureが発火する仕組み

ここでさらに内部の登場人物でobserversが出てくる、これはresolve、reject、cancelなどの指定したclosureをObserverというenumの配列として管理してくれている

簡略化した定義は以下

public indirect enum Observer {
    public typealias ResolveObserver = ((Value) -> ())
    public typealias RejectObserver = ((Error) -> ())
    public typealias CancelObserver = (() -> ())
    
    case onResolve(_: Context, _: ResolveObserver)
    case onReject(_: Context, _: RejectObserver)
    case onCancel(_: Context, _: CancelObserver)
}

これだけ見ると色々拡張して追加できそうであるがpublicなので意図としてはこれ以上は拡張したくないとも取れそう

終わりに

HydraのPromiseのベースの仕組みに絞ってコードレビューをしてみたけど、やっぱりシンプルさは良い感じである

次はPromiseを使う上で毎回お世話になるthencatchオペレータについて深掘りしていきたい