post Image
Swiftでasync/awaitな書き方もできるPromiseライブラリHydra

Promiseは非同期処理を直列実行したい時に問題となるコールバック地獄を解決したり、並列実行したそれぞれの処理の終了タイミングを制御することのできる非同期処理のデザインパターンの1つです。

SwiftではPromiseKitやSwiftTaskが有名ですね。

また、最近ではRxなどのReactive系ライブラリを使ってPromise処理をすることが多いと思います。

今回はasync/awaitライクなこともできるPromiseライブラリのHydraの使い方や解説をします。

Hydraとは

Hydra

SwiftDateSwiftLocationの作者であるmalcommacさんが作成しています

Hydraの使い方

まずは、Hydraの使い方を簡単に紹介します。

使い方はPromiseというオブジェクトを生成して、様々なオペレーターをチェーンしていきます。
Promiseを知っている方なら下記のサンプルコードは特に違和感ないと思います。


Promise { resolve, reject in

    //何かしらの非同期処理などを書く
    //処理完了後にresolveもしくはrejectを叩く
    resolve("hachinobu")

}.then { result in
    //resolveならここ
    print(result) //hachinobu
}.catch { error in
    //rejectならここ
}

では、HydraのPromiseオブジェクトや各オペレーターについて解説していきます。

Promiseオブジェクト

Promiseの前提知識

まず、前提知識としてPromiseは、保留中(pending),解決(resolve),拒否(reject)の3つの状態を持ちます。
そして、その状態に紐づく値が存在します。

状態 保持する値の型
保留中(pending) なし
解決(resolve) Value (ジェネリック型)
拒否(reject) Error

保留中(pending)は、まだPromiseでラップした処理が実行されていないことを表しており、この状態に紐づく値は存在しません。
解決(resolve)はジェネリック型によりPromiseオブジェクトの生成時に型が決まります。
拒否(reject)Errorプロトコルに準拠したエラー情報の値を保持します。

原則として、Promiseの状態遷移が発生するのは保留中(pending)状態の時のみであり、1度、解決(resolve)もしくは拒否(reject)に状態遷移した後は変わることはありません。

Promiseを使うということは、状態遷移の連鎖に紐づく処理を書くということです。

Promiseオブジェクトの生成

Promiseオブジェクトの生成は、イニシャライザの引数となるクロージャに非同期処理を書きます。
その非同期処理の結果に応じて 解決 or 拒否の状態を決めるクロージャを実行します。


//画像取得の非同期処理をラップしたPromiseオブジェクト
let fetchImagePromise = Promise<UIImage> { resolve, reject in
    //画像を取得する非同期処理
    Session.send(request) { result in 
        switch result {
        case .success(let image):
            resolve(image)
        case .failure(let error):
            reject(error)
        }
    }
}

上記は、解決状態の時にUIImage型の値を保持するPromiseオブジェクトを生成しています。

イニシャライザで書いた非同期処理では、その処理の結果を経てresolveもしくはrejectクロージャを実行する必要があります。

resolveには、そのPromiseが解決状態の場合に保持する型(UIImage)の値を引数として渡し、rejectには拒否状態の場合に保持するError情報を渡します。

Promiseにラップされた処理が実行され、resolveもしくはrejectのクロージャが実行されると、そのPromiseオブジェクトの状態遷移が発生し、状態と値が確定します。

ちなみにPromiseオブジェクトのイニシャライザの全容は

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

となっており、第一引数のContext型はHydra独自の列挙型であり、これは、第二引数のBodyクロージャを実行するスレッドの種類を指定できます。
スレッドの種類はmain,background,userInteractive,userInitiated,utility,custom(queue: DispatchQueue)と優先度も含めDispatchQueueで用意されているものと同じものが定義されています。

第二引数のBody型はresolverejectクロージャを引数に取り、主に非同期処理をするクロージャです。
(サンプルでは画像取得の非同期処理を書いた部分です)

また、デフォルト引数によりcontextにはnilが代入されるので、第一引数は指定せず、

let fetchImagePromise = Promise<UIImage> { resolve, reject in
    //bodyクロージャの処理
}

というような書き方が可能です。

contextを指定しなかった場合は、第二引数のbodyクロージャはメインスレッドで実行されます。

下記はContext型を指定して第二引数のbodyクロージャを実行するスレッドを明示的にしている例です。

Promise(in: .background) { resolve, reject in
    //Backgroundスレッドで実行される処理
}

ちなみにイニシャライザで書いた非同期処理は、Promiseオブジェクトを生成しただけでは、まだ実行されていません。
生成したPromiseオブジェクトに対して、次から説明するオペレーターをチェーンすることで処理が走ります。

また、これまでで使用した下記のイニシャライザで生成したPromiseオブジェクトの状態はpendingです。

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

これとは別に、解決(resolve)拒否(reject)状態のPromiseを生成するイニシャライザもあります。

public init(resolved value: Value)
public init(rejected error: Error)

注意

本記事ではPromiseの解決状態、拒否状態という単語を使ってこれ以降の説明をしています。

もしかすると、厳密な意味合いでは少しズレますが、
解決 = 処理の成功
拒否 = 処理の失敗
と読み替えたほうが直感的で分かりやすいかもしれません。

Promiseでラップした処理が成功した場合は、そのPromiseを解決状態に、Promiseでラップした処理が失敗した場合は、拒否状態にするというのが一般的な使い方だと思いますので。

オペレーター

Promiseオブジェクトを生成しただけでは、まだ何も起きません。
Promiseでラップした処理を実行させ、Promiseの状態遷移に応じて行いたい処理があるはずです。
ここからは、Promiseの状態遷移に応じて使えるHydraのオペレーターを紹介します。

各オペレーターはPromiseオブジェクトのインスタンスメソッド、またはグローバル関数で定義されています。

then

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

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

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

thenは、チェーン元のPromiseが解決状態の場合に実行する処理を書きます。
(チェーン元のPromiseの処理でresolveが実行された場合)

Promiseオブジェクトの生成のサンプルコードで使用したPromise<UIImage>の変数であるfetchImagePromiseで画像取得処理が成功し、resolve(image)が実行されると、チェーン元のPromiseの値imageが引数としてthenの処理が実行されます。

下記は、画像取得のAPIを叩いて取得できたら、UIを更新するサンプルです。

//fetchImagePromiseは画像を取得する非同期処理をラップしたPromise
fetchImagePromise.then { image in
   // 画像取得できた場合
   self.myImageView.image = image
}

Promise<UIImage>であるfetchImagePromise解決状態になった場合にthenの処理が実行されます。

また、Promiseオブジェクト同様、thenの第一引数はContext?となっていて、デフォルト引数はnilです。
指定しなかった場合、thenオペレーターは、第二引数のbodyクロージャをメインスレッドで実行します。
なので、UIの更新処理をする場合でも、下記のように明示的にメインスレッドを指定する必要はありません。

.then(in: .main) { image in
    //UI更新処理
}

なお、thenオペレーターは3種類あり、上記のサンプル以外にもbodyクロージャの中で、解決状態時にチェーン元とは違う型の値を保持するPromiseオブジェクトPromise<N>を返すパターンと、新たにオブジェクトNを返すthenが存在します。

この辺りは後述するPromiseチェーンの説明で使います。

catch

public func `catch`(in context: Context? = nil, _ body: @escaping ((Error) throws -> (Void))) -> Promise<Void>

catchは、チェーン元のPromiseが拒否状態の場合に実行する処理を書きます。
(チェーン元のPromiseの処理でrejectが実行された場合)

下記は、画像取得が失敗した場合にエラーをハンドリングするサンプルです。

fetchImagePromise.catch { error in
    //Errorを使った処理
}

Promise<UIImage>であるfetchImagePromise拒否状態になった場合にcatchの処理が実行されます。
引数にはreject(error)で指定したerrorが渡ってきます。

ちなみにthenオペレータ同様、catchの第一引数Context?を指定しなかった場合は、catchの処理はメインスレッドで実行されます。

Promiseチェーン

Promiseは解決拒否の状態を持つので、これまでのサンプルのようにthenのみcatchのみチェーンする場合はあまり無いと思います。

解決拒否状態の両方に対応する場合thenに対してcatchもチェーンで繋ぎます。

下記は画像取得が成功した場合と失敗した場合に対応するサンプルコードです。

fetchImagePromise.then { image in
    //fetchImagePromiseが`解決`状態(画像取得成功時)の処理
    self.myImageView.image = image
}.catch { error in
    //fetchImagePromiseが`拒否`状態(画像取得失敗時)の処理
}

この場合、画像取得の非同期処理をラップしたPromiseの状態が解決になった場合はthenの処理が実行され、拒否になった場合はcatchの処理が実行されます。
thencatchオペレーターの返り値がPromiseオブジェクトであることから、こういった書き方が可能なのです。

また、thencatchの処理が両方とも呼ばれることはありません。
仮に非同期処理をラップしたPromiseを下記のように書いても両方呼ばれることはありません。

Promise { resolve, reject in
    //何かしらの非同期処理
    //処理が完了して、resolveとrejectを2つ呼ぶ
    resolve(result)
    reject(error)
}.then { result in
    //解決状態の処理
}.catch { error in
    //拒否状態の処理
}

この場合は、大元のPromiseの処理で先にresolveを実行しているので、thenの処理のみ呼ばれます。

これは、Promiseオブジェクトの説明で記載した通り、原則としてPromiseの状態遷移は保留中(pending)からの1度のみになっているためです。
なのでresolveの後にrejectを実行しても、そのPromiseの状態は解決のまま変わりません。

ちなみにthenよりも先にcatchでチェーンした場合はthenに渡ってくる引数の値は、大元のPromiseの処理が成功して解決状態になっていたとしてもVoidです。

Promise<String> { resolve, reject in
    //何かしらの非同期処理 resultはString型
    resolve(result)
}.catch { error in
    //拒否
}.then { result in
    //解決
    //resultはString型ではなく、Void型になる
}

なぜかというと、catchの返り値がPromise<Void>になっているので、Promise<Void>thenをチェーンしているためです。
thenのチェーン元catchで返されるPromiseは解決状態の時にVoidを保持するPromiseだからです。

このことから分かるのは、大元のPromiseが解決状態になったとしてもcatchオペレーターの処理自体は行われているということです。(当然といえば当然ですが。。)

コードを見た感覚としてcatchが飛ばされているように見えているだけで、単にcatchオペレーターの引数に書いたクロージャをcatchの処理内で呼び出していないだけなのです。

このサンプルコードの一連の流れで大元のPromise,catchの返り値のPromise,thenの返り値のPromiseという3つのPromiseが生成され、それら全ては解決状態のPromiseです。(thenが呼ばれる場合)

Promiseチェーンで直列実行

thenオペレーターを複数チェーンすることで、ある処理が終わったら次にこの処理をするといった、順番通りに処理を実行する直列実行ができます。

thenオペレーターにはチェーン元のPromiseが取りうる型とは違う型のPromiseオブジェクトを生成できるインターフェースが用意されているので、柔軟に解決状態の取りうる型の違うPromiseを一連のチェーンの中で生成できます。

例えば、ユーザーIDを取得する非同期処理をして成功した場合は、取得したユーザーIDから、そのユーザーのアバター画像を取得するAPIを叩いて、画像が取得できたら画面に表示するといった処理をPromiseの直列実行で書くと下記のようになります。


Promise { resolve, reject in
    //userIdを取得する非同期処理があるとする
    //成功したのでresolveを実行しPromiseの状態を`解決`、値を"hachinobu"に確定
    resolve("hachinobu")
}.then { userId -> UIImage in
    //チェーン元のPromiseの解決状態の値userId("hachinobu")を使ってアバター取得APIを叩くとする
    let request = Request(userId: userId)
    Session.send(request) { result in 
        switch result {
        case .success(let avatar):
            resolve(image)
        case .failure(let error):
            reject(error)
        }
    }
}.then { avatar in
    //1つめのthenのPromiseが解決状態になると画像が渡ってくる
    self.myImageView.image = avatar
}.catch { error in
    //一連の処理でPromiseが拒否状態になった場合にくる
}

まず、1つ目のthenの処理で、チェーン元のPromiseの値を元に画像を取得する処理を行い、UIImageを返しています。
ここで新たにPromise<UIImage>(resolve: avatar)のオブジェクトが生成され、1つ目のthenの返り値となります。
2つ目のthenは、解決状態の時にUIImage型の値を保持するPromiseに対してチェーンしているので、UIImage型のavatarが取得できるのです。

always

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

alwaysはチェーン元のPromiseの状態が解決,拒否のどちらの場合でも実行される処理を書きます。

例えば、通信する際にローディング中のインジケータを出しておいて、alwaysでそのインジケータを消すといった、どちらの結果であっても行うべき処理を書きます。

fetchImagePromise.then { image in
    self.myImageView.image = image
}.catch { error in
    //エラー処理
    }.always(in: .main) {
    //明示的に.mainを指定して、このクロージャをメインスレッドで実行させる
}

注意点としてalwaysは、第一引数のContext?を指定しなかった場合、バックグラウンドスレッドで処理を実行するので、UI更新処理を書く場合は.mainを指定する必要があります。

defer

public func `defer`(in context: Context? = nil, _ seconds: TimeInterval) -> Promise<Value>

deferはチェーン元のPromiseが解決状態の場合に、deferの後続のオペレーターの処理をseconds引数で指定した秒数遅延させることができます。
厳密にいうとdeferオペレーターで返されるPromiseの状態遷移をseconds秒遅延させます。
(保留中から解決or拒否の状態遷移を遅延することで、後続のオペレーターは動作しないため)

下記は画像を取得してから2.0秒後に画面に反映されるサンプルコードです。

fetchImagePromise.defer(2.0).then { image in
    //画像取得成功(resolve(image))から2.0秒後に呼ばれる
    self.myImageView.image = image
}.catch { error in
    //fetchImagePromiseの状態が拒否の場合はスグに呼ばれる
}

fetchImagePromiseで画像の取得に失敗した場合は、遅延は起きません。
defer拒否状態のPromiseの場合は、拒否状態のPromiseを即座に返すので、catchオペレータの処理が実行されます。

retry

public func retry(_ attempts: Int = 3, _ condition: @escaping ((Int, Error) throws -> Bool) = { _ in true }) -> Promise<Value>

retryを使うと、チェーン元のPromiseが拒否状態の場合に、チェーン元のPromiseの処理を、第一引数で指定した回数まで再実行させることができます。

第二引数のconditionは、後何回実行できるかの回数チェーン元Promiseの拒否状態の値(Error)を引数に受け、チェーン元のPromiseの処理を再実行させるか判定するクロージャを書きます。

下記は画像取得に失敗しても最大2回は再び画像取得の処理を試みます。

fetchImagePromise.retry(2) { (count, error) -> Bool in
    //ここでcountとerrorの引数をもとにして再実行するか判定する
    return true
}.then { image in
    self.myImageView.image = image
}.catch { error in
    //エラー処理
}

また、retryにはデフォルト引数があり、最大再試行回数はattempts: Int = 3、再試行するかはcondition: @escaping ((Int, Error) throws -> Bool) = { _ in true }となっているので

//画像取得に失敗したら最大3回まで再実行する
fetchImagePromise.retry().then { image in
    self.myImageView.image = image
}.catch { error in
    //エラー処理
}

だったり、

//画像取得に失敗したら最大2回まで再実行する
fetchImagePromise.retry(2).then { image in
    self.myImageView.image = image
}.catch { error in
    //エラー処理
}

といった書き方や

fetchImagePromise.retry { (count, error) in
    //ここで再実行するか判定する
    return true
}.then { image in
    self.myImageView.image = image
}.catch { error in
    //エラー処理
}

といった書き方ができます。

(余談としてretryはチェーン元のPromiseの状態を拒否から保留中に遷移させることで、これを実現しています。)

recover

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

recoverはチェーン元のPromiseが拒否状態の場合に、その値であるerrorを引数に受け取り、新たにチェーン元と同じ型を保持するPromiseオブジェクトを返すリカバリ処理を書く事ができます。

先に書いたretryオペレータとの違いは、チェーン元のPromiseが拒否状態の場合に1度だけ行う処理であるということ、そして実行するリカバリ処理はチェーン元のPromiseの処理とは関係のない処理ができるところです。

つまり、retryはチェーン元のPromiseの状態を拒否から解決にさせようとするアプローチなのに対して、recoverrecoverが生成するPromiseオブジェクト自身の状態を解決にすべくアプローチが取れます。

例えば、画像取得に失敗した場合にエラーとして終わらせるのでなく、デフォルトの画像を差し込む場合に下記のようにretryを使うと良いのではないでしょうか。

//画像取得に失敗し、fetchImagePromiseの状態は拒否とする
fetchImagePromise.recover { error -> Promise<UIImage> in
    //処理は失敗したけどデフォルト画像を差し込ませる
    return Promise(resolve: UIImage(named: "default.png")!)
}.then { image in
    //default.pngが渡ってくる
    self.myImageView.image = image
}.catch { error in
    //fetchImagePromiseの状態は拒否だが、recoverで解決状態のPromiseになっているので、ここは呼ばれない
}

thenの処理は実行されますが、catchは実行されません。
fetchImagePromiseは画像取得できず拒否状態ですが、recover解決状態のPromiseが発行されているので後続のチェーンは解決状態のPromiseにチェーンしているためです。

recoverのクロージャにはerrorが渡されるので、そのエラー内容に応じたリカバリ処理を書けます。

ちなみにrecoverの第一引数Context?を指定しなかった場合は、バックグラウンドスレッドでrecoverの処理が実行されます。

validate

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

validateは、チェーン元のPromiseが解決状態の場合に、その値を引数に指定した条件で評価します。
条件を満たしていれば解決状態のPromise、満たしていなければ拒否状態のPromiseを返します。

例えば、通信処理が成功した場合にUserオブジェクトを取得できるが、そのUserオブジェクトのnameプロパティがnilだった場合は失敗(エラーである)とみなしたいといった場合、下記のように書けば要件を満たせます。


Promise<User> { resolve, reject in
    //User情報を取得する処理
    //成功するとUserオブジェクトが取得できるものとする
    Session.send(request) { result in 
        switch result {
        case .success(let user):
            resolve(user)
        case .failure(let error):
            reject(error)
        }
    }
}.validate { user in
    //userオブジェクトのnameプロパティのnil判定
    return user.name != nil
}.then { user in
    //user.nameがnilでない場合に呼ばれる
}.catch { error in
    //User情報取得失敗 or 取得したUser情報のnameがnil
}

Promise<User>解決状態の場合、保持している値のUserオブジェクトのnameプロパティがnilでないか評価し、nilで無ければ、解決状態のPromise<User>を生成し、nilだった場合は拒否状態のPromiseをvalidateオペレーターは返します。

そもそも最初のPromise<User>拒否状態の場合は、validateの評価の処理は呼び出されません。
その場合は、validateでもthenの処理でも拒否状態のPromiseが生成されcatchの処理が実行されます。

timeout

public func timeout(in context: Context? = nil, timeout: TimeInterval, error: Error? = nil) -> Promise<Value>

timeoutは、チェーン元のPromiseの状態が指定した秒数経過しても解決or拒否にならない場合に、第三引数で指定したエラーを持つ拒否状態のPromiseを発行します。

下記は画像取得処理が10秒経過しても終了しない場合はエラーとして、catchの処理が呼ばれるサンプルです。

fetchImagePromise.timeout(timeout: 10.0).then { image in
    self.myImageView.image = image
}.catch { error in
    //fetchImagePromiseの処理に失敗もしくは10秒経過しても終了しない場合
}

timeoutの第一引数contextと第三引数errorにはデフォルト引数が用意されているので、第二引数のtimeoutだけ指定する書き方ができます。

ちなみに第三引数のerrorを指定しなかった場合、PromiseErrortimeoutというエラーを持つ拒否状態のPromiseが発行されるのでcatchの処理の引数にはtimeoutが渡ります。

PromiseErrorErrorプロトコルに準拠したHydra独自のenumです。(timeoutは列挙子)

pass

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

passはチェーン元のPromiseが解決状態の場合に、その値を元にして独自の処理を行い、処理が正しく処理された場合は、チェーン元のPromiseと同じ状態のPromiseを返します。
passの独自処理で、その処理が失敗した場合は、拒否状態のPromiseを返します。

例えば、あるデータをAPIから取得した後に、そのデータの整合性を判定するAPIがあるとして、そこに通信処理をして、結果が正しければ、取得したデータをそのまま後続で処理させるが、結果が誤りという結果になった場合は、Promiseの状態を拒否にして、それ以降の処理をさせないといった場合でしょうか。


//あるデータを取得
Promise { resolve, reject in
    Session.send(request) { result in
        switch result {
        case .success(let data):
            resolve(user)
        case .failure(let error):
            reject(error)
        }
    }
}.pass { data -> Promise<Void> in
    //渡ってきたdataの整合性を確認する処理
    //この処理が正の場合はチェーン元と同じPromiseオブジェクトを返す
        //誤の場合は、後続のチェーンオペレーターに`拒否`状態のPromiseを返す
    return Promise<Void> { resolve, reject in

        let request = Request(data: data)
        Session.send(request) { result in
            switch result {
            case .success(let _):
                //正の場合
                return Promise(resolved: ())
            case .failure(let error):
                //誤の場合
                return Promise(rejected: error)
            }
        }
    }

}.then { data in
    //整合性の取れているdataがくる
}.catch { error in
    //整合性が取れなかった or Dataが取れなかった
}

このように一連の処理の中で、ある処理を挟み、その処理が失敗した場合には、後続の処理をさせないといった感じです。

all

public func all<L, S: Sequence>(_ promises: S, concurrency: UInt = UInt.max) -> Promise<[L]> where S.Iterator.Element == Promise<L>

allを使うと非同期処理を並列実行して、その終了のタイミングをハンドリングすることが可能です。
このオペレーターはグローバル関数として定義されています。

引数として、Promiseの配列と、その処理の同時実行数concurrencyを指定できます。
配列内のPromiseオブジェクトは、解決状態の型が全て同じPromiseオブジェクトである必要があります。

allは配列内のPromiseの処理を並列実行して、その全てのPromiseが解決状態になれば、各Promiseの値を配列として持つPromiseオブジェクトが返されます。
どれか1つでも拒否状態になった時点で拒否状態のPromiseが返されます。
他のPromiseが保留中(pending)でも、その処理は待たずに拒否状態のPromiseを返します。

例えば、ユーザーIDを指定して、そのユーザーのいいね数を取得できるAPIがあるとします。
ユーザーID hachinobu,hoge,fugaいいね数をそれぞれ取得する処理のPromiseを生成し、それらの処理を並列実行して、全て成功した場合に合計のいいね数を表示するといったことをやりたい時は下記のように書けます。


//いいね数を取得するPromiseを生成
func fetchLikeCountPromise(userId: String) -> Promise<Int> {
    return Promise<Int> { resolve, reject in
        let likeCountRequest = LikeCountRequest(userId: userId)
        Session.send(likeCountRequest) { result in
            switch result {
            case .success(let likes):
                resolve(likes)
            case .failure(let error):
                reject(error)
            }
        }
    }
}

//それぞれのユーザーのいいね数を取得するPromiseの配列
let likeCountPromises = ["hachinobu", "hoge", "fuga"].map(fetchLikeCountPromise)

all(likeCountPromises).then { likeCounts in

    //likeCounts = [100, 130, 40]
    //それぞれのPromiseの処理の結果が配列で返る
    let displayLikeSum = likeCounts.reduce(0) { (sum, likeCount) in
        return sum + likeCount
    }.description

    self.myLabel.text = displayLikeSum

}.catch { error in

    //配列のPromise処理が1つでも失敗したら実行される

}

allの引数で指定した配列内の全Promiseの処理が実行され、全Promiseが解決状態になると、thenが実行されます。
thenに渡ってくる値は、配列内の各Promiseの解決状態の時に取る値の配列です。

allの引数に渡した配列内のPromiseが1つでも拒否状態の場合は、thenの処理は実行されず、catchが実行されます。

any

public func any<L>(in context: Context? = nil, _ promises: [Promise<L>]) -> Promise<L>

anyは配列のPromiseの中で一番最初に状態が確定したPromiseを返します。

anyall同様に解決状態の値が同じ型のPromiseの配列を引数に渡します。
anyは引数に渡した配列内のPromise処理を並列実行させ、一番最初に状態が確定(解決 or 拒否)したPromiseオブジェクトが返ります。

JSのPromiseのraceと同じでしょうか。

下記は複数ユーザーの画像の取得処理を並列実行させ、最初に取得できたユーザーの画像を表示するサンプルです。


//画像を取得する処理をラップしたPromiseを生成
func fetchAvatarImagePromise(userId: String) -> Promise<UIImage> {
    return Promise<UIImage> { resolve, reject in
        let avatarRequest = AvatarRequest(userId: userId)
        Session.send(avatarRequest) { result in
            switch result {
            case .success(let avatar):
                resolve(avatar)
            case .failure(let error):
                reject(error)
            }
        }
    }
}

//それぞれのユーザーのAvatar画像を取得するPromiseの配列
let avatarPromises = ["hachinobu", "hoge", "fuga"].map(fetchAvatarImagePromise)

any(avatarPromises).then { image in
    //一番最初に返ってきたPromiseオブジェクトが`解決`状態だった場合に、そのユーザーのavatar(UIImage)が渡される
    self.firstAvatarImageView.image = image
}.catch { error in
    //一番最初に返ってきたPromiseオブジェクトが`拒否`状態だった場合に呼ばれる
}

このように並列実行して一番早く返ってきた結果に基づいて処理したい場合に使います。
2番目、3番目のPromiseの状態が解決もしくは拒否だろうと関与しません。

zip

public static func zip<A, B>(in context: Context? = nil, _ a: Promise<A>, _ b: Promise<B>) -> Promise<(A,B)>

public static func zip<A,B,C>(in context: Context? = nil, a: Promise<A>, b: Promise<B>, c: Promise<C>) -> Promise<(A,B,C)>

public static func zip<A,B,C,D>(in context: Context? = nil, a: Promise<A>, b: Promise<B>, c: Promise<C>, d: Promise<D>) -> Promise<(A,B,C,D)>

zipallと同じように複数の非同期処理の終了タイミングなどをハンドリングができます。
allのように同じ型の値を取りうるPromiseである必要はありません。

処理を経て、引数に指定したPromiseが全て解決状態になると、それぞれの値をタプルで保持した解決状態のPromiseが返ります。

zipはPromiseクラスの静的関数として定義されています。
現在用意されているインターフェースは解決状態の時に取りうる値の型が違うPromiseを最大4つまで指定できます。
引数にはそれぞれ並列実行したいPromiseオブジェクトを渡すだけです。

例えば、ある画面にユーザー情報を表示するために、ユーザー情報のAPIとユーザーのアバターを取得するAPIを叩く必要があるとした場合、APIを叩く処理をそれぞれ並列に実行して、両方が終わったタイミングで取得できた型の違うObjectを元に、画面の表示用に使うViewModelを作るようなケースに使えます。


//ユーザー情報(オブジェクトUser)を取得する処理のPreomise
let fetchUserPromise = Promise<User> { resolve, reject in
    let request = UserRequest(id: "hachinobu")
    Session.send(request) { result in
        switch result {
        case .success(let user):
            resolve(user)
        case .failure(let error):
            reject(error)
        }
    }
}

//ユーザーのアバター画像(UIImage)を取得する処理のPromise
let fetchAvatarPromise = Promise<UIImage> { resolve, reject in
    let request = AvatarRequest(id: "hachinobu")
    Session.send(request) { result in
        switch result {
        case .success(let avatar):
            resolve(avatar)
        case .failure(let error):
            reject(error)
        }
    }
}

Promise<Void>.zip(fetchUserPromise, fetchAvatarPromise).then { (user, avatar) in
    //fetchUserPromiseとfetchAvatarPromiseの処理が共に成功した場合に呼ばれる
    //引数として各Promiseのresolveの値がタプルで取得できる
    self.userViewModel = UserViewModel(user: user, avatar: avatar)
    self.tableView.reloadView()
}.catch { error in
    //fetchUserPromiseもしくはfetchAvatarPromiseのいずれかが失敗した場合
}

reduce

public func reduce<A, I, S: Sequence>(in context: Context? = nil, _ items: S, _ initial: I, _ transform: @escaping (I, A) throws -> Promise<I>) -> Promise<I> where S.Iterator.Element == A

reduceは、Swift言語でお馴染みのreduceと同じように、Sequenceの要素をまとめる処理をした結果の値を解決状態の時に保持するPromiseを生成することができます。

例えばallオペレーターの際に説明した、指定ユーザーのいいね数を表示するサンプルコードとしてallオペレータにチェーンしたthenオペレーターの処理内で、それぞれのいいね数の配列([Int])を使って集計しましたが、allオペレーターにチェーンしたthen処理内でreduceを使って合計値を保持したPromiseオブジェクトを作ることができます。


//likeCountPromisesはPromise<Int>の配列
all(likeCountPromises).then { likeCounts -> Promise<Int> in

    //likeCounts = [100, 130, 40]
    //reduceを使って各ユーザーの`いいね数`の合計のPromise<Int>を生成
    return reduce(likeCounts, 0) { (totalCount, likeCount) in 
        let total = totalCount + likeCount
        return Promise(resolved: total)
    }

}.then { total in
    //reduceで生成されたPromise<String>のresolveの値が渡ってくる
    self.myLabel.text = total.description
}.catch { error in

}

async/await

Hydraはasync/awaitが使えます。
これを使えば非同期処理を同期的であるかのように書くことができます。

public func async(in context: Context, after: TimeInterval? = nil, _ block: @escaping (Void) -> (Void)) -> Void


@discardableResult
public func await<T>(in context: Context = .background, _ body: @escaping ((_ fulfill: @escaping (T) -> (), _ reject: @escaping (Error) -> () ) throws -> ())) throws -> T

使い方は、グローバル関数であるasyncのクロージャ内で、awaitを使って非同期処理を書きます。
例として画像を取得する非同期処理が成功したら、ImageViewに表示するという一連の流れをasync/awaitを用いると下記のように書くことができます。

async(in: .main) {

    do {
        //awaitを使って非同期処理
        let image: UIImage = try await { resolve, reject in
            Session.send(request) { result in
                switch result {
                case .success(let image):
                    resolve(image)
                case .failure(let error):
                    reject(error)
                }
            }
        }

        //取得できたimageをUIに反映
        self.myImageView.image = image
    } catch {
        //何かしらのエラー処理
    }

}

asyncの第一引数にはContext型を指定し、第二引数のクロージャを実行するスレッドを指定します。
上記サンプルではUIの更新処理をするのでasync.mainを指定しています。

awaitで書いた非同期処理が成功すると、その値が返ります。
awaitは非同期処理が終わるまで待ってくれます。
そして、awaitの非同期処理が終わってから、その下の行に書いたUI更新処理self.myImageView.image = imageが実行されます。

awaitの処理では失敗(reject)になる場合があるので必ずtryが必要になります。
また、注意点としてawaitを使う場合は、ジェネリック関数の型を確定させるために、awaitの処理の返り値を格納する変数に型を明示的に書く必要があります。(型推論はしてくれません。)

let image: UIImage

もし変数の型を書かない場合は下記のようにawaitresolveクロージャに型を書く必要があります。

let image = try await { (resolve: @escaping (UIImage) -> (), reject: @escaping (Error) -> ()) in
    Session.send(request) { result in
        switch result {
        case .success(let image):
            resolve(image)
        case .failure(let error):
            reject(error)
        }
    }
}

また、awaitにはPromiseオブジェクトを引数に受け取るインターフェースもあります。

public func await<T>(in context: Context? = nil, _ promise: Promise<T>) throws -> T

これを使うとPromiseでラップした非同期処理のインスタンスを用意しておいてawaitの引数に指定して非同期処理を同期的に実行させることができます。

async(in: .main) {

    do {

        //画像取得の非同期処理をラップしたPromiseオブジェクト
        let fetchImagePromise = Promise<UIImage> { resolve, reject in
            //画像を取得する非同期処理
            Session.send(request) { result in 
                switch result {
                case .success(let image):
                    resolve(image)
                case .failure(let error):
                    reject(error)
                }
            }
        }
        //awaitにPromiseオブジェクトを渡して非同期処理を同期的に実行する
        let image: UIImage = try await(fetchImagePromise)

        //取得できたimageをUIに反映
        self.myImageView.image = image
    } catch {
        //何かしらのエラー処理
    }

}

awaitの返り値は、引数で渡したPromiseの解決状態の値を取得することができます。
do-catchを使うのは、Promiseの拒否状態を考慮して、エラーを検知、処理するためです。

先に紹介した

@discardableResult
public func await<T>(in context: Context = .background, _ body: @escaping ((_ fulfill: @escaping (T) -> (), _ reject: @escaping (Error) -> () ) throws -> ())) throws -> T

こちらのawaitでも結局は中でPromiseを生成しています。
使う側がPromiseオブジェクトを意識しないでも書けるインターフェースになっているだけです。

awaitのシンタックスシュガー

awaitにはシンタックスシュガーが用意されています

prefix operator ..
public prefix func ..<T> (_ promise: Promise<T>) throws -> T

これをawaitの代わりに使えます。
引数としてPromiseオブジェクトを受け取ります。


async(in: .main) {

    do {
        let fetchImagePromise = Promise<UIImage> { resolve, reject in
            Session.send(request) { result in
                switch result {
                case .success(let image):
                    resolve(image)
                case .failure(let error):
                    reject(error)
                }
            }
        }
        //awaitの代わりに..を使う
        let image: UIImage = try ..(fetchImagePromise)

        //取得できたimageをUIに反映
        self.myImageView.image = image
    } catch {
        //何かしらのエラー処理
    }

}

また、awaitを使った時にエラーの内容は特に気にしないのであれば、awaitの代わりに..!を使えばdo-catchを書く必要がなくなります。

prefix operator ..!
public prefix func ..!<T> (_ promise: Promise<T>) -> T?

これを使うと処理が失敗した場合は、nilが返ってきます。
try awaitではなくtry? awaitと書いた場合と同じ挙動になるということです。


async(in: .main) {

    let fetchImagePromise = Promise<UIImage> { resolve, reject in
        Session.send(request) { result in
            switch result {
            case .success(let image):
                resolve(image)
            case .failure(let error):
                reject(error)
            }
        }
    }
    //..!を使う
    let image: UIImage? = try ..!(fetchImagePromise)

    guard let image = image else {
        //何かしらのエラー処理
        return
    }

    //取得できたimageをUIに反映
    self.myImageView.image = image

}

async/awaitでもPromiseチェーン

asyncにはもう1つインターフェースが用意されていて、こちらは返り値としてPromiseオブジェクトを返すのでPromiseチェーンが使えます。

public func async<T>(in context: Context? = nil, _ body: @escaping ( (Void) throws -> (T)) ) -> Promise<T>
async { _ -> UIImage in

    let image: UIImage = try await { (resolve, reject) in
        Session.send(request) { result in
            switch result {
            case .success(let image):
                resolve(image)
            case .failure(let error):
                reject(error)
            }
        }
    }

    return image

}.then { image in
    //awaitの処理が成功した場合
    self.myImageView.image = image
}.catch { error in
   //awaitの処理が失敗した場合
}

こちらを使った場合はasyncが返したPromiseオブジェクトをthenなどのオペレーターでチェーンするまで処理が動きません。(asyncの返り値のPromiseは保留中状態ということです。)

先で説明した返り値のないasyncとは混同しないように注意してください。

async/awaitで直列実行

async/awaitを使うと非同期処理の直列実行の可読性がPromiseチェーンを用いた場合よりも上がります。

例として、全ユーザー情報を返すAPIを叩いて、Userオブジェクトの配列を取得した後に、先頭のユーザーのIDをもとに、そのユーザーの投稿した記事の一覧、Articleオブジェクトの配列を取得したい場合を想定して書くと下記のように表現できます。


async (in: .main) {

    var articleList: [Article] = []
    do {
        //ユーザー一覧を取得
        let userList: [User] = try await { resolve, reject in
            Session.send(request) {
                switch result {
                case .success(let userList):
                    resolve(userList)
                case .failure(let error):
                    reject(error)
                }
            }
        }

        //ユーザー一覧配列の先頭のユーザーのidを指定して、そのユーザーの投稿一覧を取得
        let userId = userList.first.id
        articleList = try await {
            //投稿一覧取得のRequest
            let request = ArticleListRequest(userId: userId)
            Session.send(request) {
                switch result {
                case .success(let articleList):
                    resolve(articleList)
                case .failure(let error):
                    reject(error)
                }
            }
            Session.send()
        }

    } catch {
        //エラー処理
        //ユーザー取得もしくは投稿取得のどちらでエラーが発生してもここにくる
        //これ以降の処理をさせない
        return
    }

    //`articleList`を使ったりしてUI更新処理などをする
    self.viewModel = MyViewModel(source: articleList)
    self.tableView.reloadData()

}

このようにユーザー一覧を取得する非同期処理特定のユーザーの投稿一覧を取得する非同期処理を同期的に書く事ができます。

本来、awaitasyncの中でしか使えないと思いますが、Hydraではawaitはグローバル関数で定義されているので、asyncの中で無くても使えてしまいます。

ちなみにawaitDispatchSemaphoreのvalueを0にして、処理が終わるまでwaitさせることで実現しています。(処理が完了したらsignalして後続の処理をするといった感じです。)

最後に

軽量で高機能なHydraを紹介してみました。
実現方法など、ソースコードの中身まで解説できれば良かったのですが思った以上に長い記事になってしまったので今回はここまでです。
気軽にソースコードを読んでみるだけでも勉強になると思いますので、気になった方は是非読んでみると面白いと思います。


『 Swift 』Article List