Promiseは非同期処理を直列実行したい時に問題となるコールバック地獄を解決したり、並列実行したそれぞれの処理の終了タイミングを制御することのできる非同期処理のデザインパターンの1つです。
SwiftではPromiseKitやSwiftTaskが有名ですね。
また、最近ではRxなどのReactive系ライブラリを使ってPromise処理をすることが多いと思います。
今回はasync/awaitライクなこともできるPromiseライブラリのHydraの使い方や解説をします。
Hydraとは
SwiftDateやSwiftLocationの作者である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
型はresolve
、reject
クロージャを引数に取り、主に非同期処理をするクロージャです。
(サンプルでは画像取得の非同期処理を書いた部分です)
また、デフォルト引数により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
の処理が実行されます。
then
やcatch
オペレーターの返り値がPromiseオブジェクトであることから、こういった書き方が可能なのです。
また、then
とcatch
の処理が両方とも呼ばれることはありません。
仮に非同期処理をラップした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の状態を拒否
から解決
にさせようとするアプローチなのに対して、recover
はrecover
が生成する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を指定しなかった場合、PromiseError
のtimeout
というエラーを持つ拒否
状態のPromiseが発行されるのでcatch
の処理の引数にはtimeout
が渡ります。
PromiseError
はError
プロトコルに準拠した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を返します。
any
もall
同様に解決
状態の値が同じ型の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)>
zip
はall
と同じように複数の非同期処理の終了タイミングなどをハンドリングができます。
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
もし変数の型を書かない場合は下記のようにawait
のresolve
クロージャに型を書く必要があります。
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()
}
このようにユーザー一覧を取得する非同期処理
と特定のユーザーの投稿一覧を取得する非同期処理
を同期的に書く事ができます。
本来、await
はasync
の中でしか使えないと思いますが、Hydraではawait
はグローバル関数で定義されているので、async
の中で無くても使えてしまいます。
ちなみにawait
はDispatchSemaphore
のvalueを0にして、処理が終わるまでwaitさせることで実現しています。(処理が完了したらsignalして後続の処理をするといった感じです。)
最後に
軽量で高機能なHydraを紹介してみました。
実現方法など、ソースコードの中身まで解説できれば良かったのですが思った以上に長い記事になってしまったので今回はここまでです。
気軽にソースコードを読んでみるだけでも勉強になると思いますので、気になった方は是非読んでみると面白いと思います。