Promise は非同期処理で遅延評価やパイプライン化を行うためのよく知られたデザインパターンです。一般的に内部的に3つの状態がプロミスの振る舞いをコントロールするために使われますが、iOS または OS X の開発においては不可欠なコア・インターフェースが不足しています。このニーズを満たすために、LINE のエンジニアで ReactKit の作者である 稲見泰宏 さんは Swift のライブラリとして SwiftTask を作りました。SwiftTask における Resume や Progress の扱いを詳しく見ていくことにより、Reactive プログラミングのパラダイムに対する新しいアプローチについて調べます。
Swift は何をもたらしたのか? (2:31)
これから State、Promise、Reactive プログラミングについて、私がライブラリを Swift で書く過程で発見したいくつかのことをお話します。この Stack Overflow の調査によると、Swift はたった一年で世界で一番愛されてる言語となりました。それでは、実のところ Swift は私たちに一体、何をもたらしたでしょうか? ジェネリクスやタプルを使うことで、より型安全なコードが書けるようになりました。また、パターンマッチングなどが使える構造体や Enum のような値型もあります。簡潔な文法により関数型プログラミングが書きやすくなりました。そして、関数がついに第1級オブジェクトとなりました。
第1級オブジェクトとしての関数 (2:54)
Swift では、関数を他のデータ型と同じように扱えます。たとえば、y = f(x)
という関数について考えてみます。f
が関数で、x
は入力、y
が出力です。これには Int 型や String 型のような基本型を渡すことができます。高階関数においては、入力や出力に関数が使えるようになります。入力と出力にジェネリクスを使用することもできます。
// function in, function out
func f(x: Int -> String) -> (Bool -> Void)
// generic in, generic out
func f<T, U>(x: T) -> U
コールバック (3:41)
関数の中には、コールバックを引数として受け取る関数が定義できます。他の言い方をすれば、継続渡しスタイル の関数です。以下のような doSomething
関数を呼ぶとき、引数 arg
とともに callback
を渡します。この関数は何も返しませんが、コールバックの中でその結果を受け取ることができます。
func doSomething<A, R>(arg: A, callback: R-> Void)
// T = (A, R -> Void)
// U = Void
下記の最初の例は、コールバックが1つの例です。引数とともに doSomething
を呼び、result
をコールバックの中で受け取ります。もしその他に何か処理を行いたい場合、二番目の例のように result1
を使って doSomethingElse
を呼べば良いのです。そして result2
を別のコールバックの中で受け取ります。これを続けると、簡単にコールバック地獄に陥ります。コールバック地獄を避けようとするなら、理想的には三番目の例のように、メソッドチェーンスタイルに書きかえるのが良いです。
// 1 callback
doSomething(arg) { result in
println("done with result = \(result)")
}
// 2 callbacks
doSomething(arg) { result1 in
doSomethingElse(result1) { result2 in
println("done with result = \(result2)")
}
}
// ideally, method chaining style
doSomething(arg)
.then { result1 in doSomethingElse(result1) }
.then { result2 in println(result2) }
モナド (4:52)
では、どのようにすればこれが実現できるでしょうか?今日話すことの中には Promise も含まれていて、Promise を使えばこれが解決できます。しかし、ここでそろそろ M で始まるものについて見ていってもいい頃でしょう。それは、モナド です。おそらく、何人かの人はこの言葉を恐れていると思います。すでに十分ご存知な人もいると思います。Wikipedia で モナドの定義について確認しましたが、少し長すぎましたので、ここでは、簡潔にモナドとは何かをお伝えすることにします。
モナドはパイプライン処理を可能にするコンテナと考えることができます。以下の疑似コードを見てみると、2つメソッドを必要とする struct Monad
があります。その関数は、toMonad
と flatMap
です。toMonad
は、コンテナにラップするだけの関数です。Haskell で言う pure
や return
と同じようなものです。値をコンテナの中にラップします。flatMap
は、モナドから値を取り出し、関数 f
を適用し、新しいコンテナを返します。また、モナドがどのように動くのかについてのわかりやすい絵があります。
// pipelineable container
struct Monad<T> {
// unwrap -> transform -> rewrap (automatically)
// func map<U>(f: T -> U) -> Monad<U> // not required
// unwrap -> transform (rewrapping manually)
func flatMap<U>(f: T -> Monad<U>) -> Monad<U>
}
// wrap (Haskell's `return` or `pure`)
func toMonad<T>(value: T) -> Monad<T> {
return Monad(value)
}
モナド則 (6:38)
これらを実装することにより struct Monad
は、モナド則を満たしていると言えます。これらの3つの法則は、左恒等性、右恒等性、そして結合法則です。一旦、Haskell の文法で表すことにします。しかし、ここにいるほとんどの人が Haskell をよく知らないと思うので、return
を toMonad
と >>=
を flatMap
と解釈してください。そして、Swift でこのモナド則を書き直してみると以下のようになります。
1. toMonad(a).flatMap(f) = f(a)
2. monad.flatMap(toMonad) = monad
3. monad.flatMap(f).flatMap(g) = monad.flatMap { x in f(x).flatMap(g) }
それぞれの規則について見ていきましよう。一つ目は、a
という値があったときに、toMonad
を使ってコンテナでラップします。そして、それに flatMap(f)
を適用することで f(a)
と同じ意味になります。二つ目は、値が分からない monad
があり、そのモナドに対して flatMap(toMonad)
を行います。これは、値をモナドから取り出して、またモナドの中に入れるのと同じようなものです。元にあったものと明らかに同じになりますので、monad
と等しくなります。最後の規則は、flatMap
を二回 monad
から呼び出します。x
という値を monad
から受け取り、関数 f
を適用し、そして flatMap(g)
を呼び出します。これは最終的には、右辺と同じ意味になります。
三つ目の結合法則は非常に重要になります。もし、flatMap
を連続で呼び出していくとどうなるでしょうか? 以下のようなネストした構造になってしまいます。結合法則によって、ネストした flatMap
をパイプラインスタイルに変換することができ、コールバック地獄を避けることができるのです。これがモナドで大事なことです。
monad.flatMap { x in
return f(x).flatMap { y in
return g(y).flatMap { z in
return ...
}
}
}
// is the same as
monad.flatMap(f).flatMap(g).flatMap(h)...
いくつかモナドの例を挙げます。Optional 型や Array 型のように Swift にもモナドがあります。Swift 1.2 の Optional 型には flatMap
があります。Optionalはようやく、モナドになりました。そして、サードパーティライブラリの Result や Either もあります。これらは、処理が成功したのか失敗したのか知らせてくれます。Promise や Future もモナドの仲間です。
Promise (9:14)
では Promise とは何でしょうか?Promise とは、単一の遅延評価される値のためのコンテナです。Promise は数多くの言語に存在します。Scala では Future、Java では CompletableFuture、C# では Taskなどです。しかし、今日は Promise を Javascript の Promise としてお話しします。
では、Swift ではどうでしょうか?Swift には本当の意味の Promise はありません。しかし、作ることができます。koher さんが開発した PromiseK というプロジェクトがあります。これを使えば、Swift で Promise を以下のように扱うことができます。
class Promise<T> {
init(_ executor: (resolve: Promise<T> -> Void) -> Void)
func map<U>(f: T -> U) -> Promise<U>
func flatMap<U>(f: T -> Promise<U>) -> Promise<U>
}
この Promise
クラスでは、以前の例の toMonad
と flatMap
に当たる、イニシャライザと flatMap
が定義されています。これは、モナド則に従っています。これをどのように使えば良いでしょうか?promisify
関数というこの Promise
クラスのインスタンスを作成するヘルパー関数を作成しました。クロージャの実行の中で、コールバックスタイルの関数 f
を呼び出しています。また、result
がコールバックの中で取得できます。そして、Promise オブジェクトにタスクが終わったことを伝えるために resolve
を呼び出しています。こうすることにより、コールバックで書かなければいけなかったところが、Promise のパイプラインスタイルで書けるようになります。
// helper: wrap call of `f(arg, callback)` with Promise
func promisify<A, R>(f: (A, R -> Void) -> Void)(_ arg: A)
-> Promise<R> {
return Promise<R> { resolve in
f(arg) { result in
resolve(Promise<R>(result))
}
}
}
// Promise pipelining
promisify(doSomething)(arg)
.flatMap { result1 in promisify(doSomethingElse)(result1) }
.map { result2 in println(result2) }
then
との比較 (11:21)
doSomething
を呼び、その後、二回 .then
を行いたかったとします。ここでは、doSomething
はただの関数で then
メソッドを持っていないので動きません。しかし、PromiseK はこの問題を解決しています。PromiseK のメソッドで then
を実現するとき、doSomething
を直接呼び出すべきでないことに気付くと思います。その代わりに、promisify
を使って、Promise の世界にラップしてあげる必要があります。ここでの then
は JavaScript の例ですが、flatMap
や map
のシンタックスシュガーと見なせます。つまり、JavaScript の Promise は、モナドの一部に過ぎません。
// ideally
doSomething(arg)
.then { doSomethingElse($0) }
.then { println($0) }
// PromiseK
promisify(doSomething)(arg)
.flatMap { promisify(doSomethingElse)($0) }
.map { println($0) }
モナドでのプログラミングのルールについてお話しします。then
の良いところは、map
や flatMap
よりも読みやすく、直感的になるところです。then
を使うと、他のタスクが終わり、別のコードが実行されるという風に理解しやすくなります。また、then
は内部で状態があり、途中で中断させることも可能です。onRejected
を登録しておくことで、タスクが失敗したときに呼びだされます。
単純すぎる? (13:18)
Swift と JavaScript の Promise は、とても良い構造です。しかし、はじめに見たとき単純すぎると感じました。Promise は、コアとなるインターフェースが欠けています。Promise は成功か失敗のどちらかの状態だけを返します。しかし、処理の途中の状態が知りたいときはどうでしょうか? 処理の進捗や途中の値が知りたいのです。そして、NSOperation
でできるような、途中で中断したり、再開したり、キャンセルを行いたいのです。そこで、私は初めてこれを見たときに、もう少し頑丈で強力なものにしたいと思いました。
Promise の拡張: SwiftTask (14:23)
そこで、新しく Promise の概念を拡張した SwiftTask という Swift ライブラリを作ることにしました。Promise のステートマシンは、初期状態の後に待機状態になります。そして、最後に成功か失敗のどちらかで終わります。決して状態が戻ったりはしません。それとは対照的に、SwiftTask
は、より複雑なステートマシンを持っていて、中断したり、再開したりできます。これは、成功か失敗で終わるまで実行され、必要であれば外からキャンセルもできます。
SwiftTask は基本的には Promise と同じ機能を持っていて、その上で、中断や再開、キャンセルやリトライさえも行えます。純粋な Swift だけで書かれていて、import Foundation
や NSError
などは一切使われていません。先ほどの Promise の例では、非同期パターンを行っていましたが、同期処理を行うことも可能です。さらに、スレッドセーフです。
インターフェース (15:47)
ここで使っている SwiftTask はバージョン3.3です。同じ名前のメソッドが二つあることに気付くかと思います。それぞれ map
版と flatMap
版です。また、結果が失敗に終わったときに、内部でエラーが起きたのか、外からキャンセルされたのか確かめることもできます。処理が失敗に終わったとき、ErrorInfo
を確認することでキャンセルかどうか判断できます。
class Task<Progress, Value, Error>
typealias ErrorInfo = (error: Error?, isCancelled: Bool)
func then<V2>(f: (V?, ErrorInfo?) -> V2) -> Task<P, V2, E>
func then<P2, V2, E2>(f: (V?, ErrorInfo?) -> Task<P2, V2, E2>) -> Task<P2, V2, E2>
func success<V2>(f: V -> V2) -> Task<P, V2, E>
func success<P2, V2, E2>(f: V -> Task<P2, V2, E2>) -> Task<P2, V2, E2>
func failure(f: ErrorInfo -> V) -> Task
func failure<P2, E2>(f: ErrorInfo -> Task<P2, V, E2>) -> Task<P2, V, E2>
以下が拡張したうちの一部です。進捗が監視でき、それに合わせて UI を更新することができます。また、pause
, resume
, cancle
, retry
メソッドもあります。
func progress(f: (oldProgress: P?, newProgress: P) -> Void) -> Task
func pause() -> Bool
func resume() -> Bool
func cancel(error: E?) -> Bool
func try(maxTryCount: Int) -> Task
通信処理の例 (16:39)
では、どのように SwiftTask を使うのか見ていきましょう。以下が Alamofire を使って、通信処理を行っている例です。typealias AlamofireTask
を新しい SwiftTask として作成します。そして、クロージャの中で、Alamofire
の処理を記述し、progress
をに渡します。
let task = AlamofireTask { progress, fulfill, reject, configure in
// define task
Alamofire.download(.GET, "http://httpbin.org/stream/100", destination: somewhere)
.progress { newProgress in
progress(newProgress)
}
.response { request, response, data, error in
if let error = error {
reject(error)
return
}
fulfill(response)
}
return
}
.progress
にメソッドチェーンを続けて、そのクロージャの中で UI を更新する処理を書きます。そして、成功したときや失敗したときに何を行うかも記述しておきます。最後にプログレスバーを隠すなどの処理を書きます。途中で、pause
, resume
, cancel
を呼び出すことで、処理に割り込むことができます。
task.progress { oldProgress, newProgress in
// update progress UI
}.success { response -> Void in
// handle fulfilled
}.failure { error, isCancelled -> Void in
// handle rejected or cancelled
}.then { _ in
// finally
}
// running task is interruptable if configured
task.pause()
task.resume()
task.cancel()
リアクティブプログラミング (18:30)
SwiftTask から成功したときの値か、失敗したというエラーのどちらかを受け取ることになります。また、途中で進捗状況も随時、受け取ることができます。この動作について少し考えてみると、継続的に値を受け取る onNext
、空の値を受け取る onCompleted
、エラーを受け取る onError
に似ていると思います。これは Reactive Extensions や リアクティブプログラミングに非常に近いものです。また、Objective-C と Swift の世界にも ReactiveCocoa というものがあります。
Stream & ReactKit (18:49)
また、Node.js の Stream とも似ていると思いました。全てが Stream です。Stream とは、複数の遅延評価される値のためのコンテナだと言えます。つまり、何度も値を送ることができるということです。そして、これが私が Swift で開発した ReactKit です。ReactKit は、ジェネリクスの型 T
を進捗の値として使った Task
のサブクラスです。完了したときは、空のデータの Void
を受け取り、エラーが出たときは、NSError
を受け取ります。
この Stream パイプラインと複数のタスクがコラボレーションできるのが分かりますか?Stream には基本的に Hot と Cold があります。そして、簡単な バックプレッシャー メカニズムを使用することができます。“バックプレッシャー” は、リアクティブプログラミングで流行りの言葉です。ReactKit では、非常に多くのイベントを受け取ったときにアップストリームを止めるために使っています。ダウンストリームは、受け取りが再開できる状態になるまでポーズしています。
使用例 (20:31)
一つ目の例は、ReactKit で FizzBuzz を行う例です。1 から 100 の sequence
があり、それを map
で String
に変換しています。そして、場合によっては、“Fizz”、“Buzz”、“FizzBuzz” に置き換えます。
Stream.sequence(1...100)
|> map { x -> String in
switch x {
case _ where x % 15 == 0: return "FizzBuzz"
case _ where x % 3 == 0: return "Fizz"
case _ where x % 5 == 0: return "Buzz"
default: return "\(x)"
}
}
~>! println
// prints each value: 1, 2, Fizz, 4, Buzz, ...
次の例は、(0,1) から始まる、infiniteSequence
を使ったパターンです。順に値が追加され、元あった値は左にシフトしていきます。これでフィボナッチ数列が作成できます。このストリームを実行したとき、無限ループが起こってしまうのでそれを避ける必要があります。値に制限をかけるために、take(10)
を使います。最後に buffer
を使って配列に値を集めます。
let fibonacciValues =
Stream.infiniteSequence((0, 1)) { a in (a.1, a.0 + a.1) }
|> map { a in a.0 }
|> skip(3)
|> take(10)
|> buffer()
~>! () // terminal operation
println(fibonacciValues)
// [2, 3, 5, 8, 13, 21, 34, 55, 89, 144]
そして、次は KVO を使ったより実践的な例です。これはスプレッドシートの例です。cell1
と cell2
があります。そして、cell1
と cell2
が等しいと宣言しておきます。そうしておくことで、cell1
の値が変化したときに cell2
の値も自動的に更新されます。ここでやる必要があることは、cell1Stream
を作成し、cell2
にバインドします。そして、cell1.value
を更新します。
// create stream via KVO
self.cell1Stream = KVO.stream(cell1, "value")
// bind stream via KVC (`<~` as binding operator)
(cell2, "value") <~ self.cell1Stream
// cell2.value == "initial" at this point
cell1.value = "REACT"
// cell2.value == "REACT" // updates automatically
最後の例は、インクリメンタルサーチの例です。これは少し難しいです。サーチバーから textChangedStream
が取得できます。それで、全ての変更ではなく、あるタイミングや重複したものを取り除きたい場合は、制限を設定することができます。使うものは、throttle
と distinctUntilChanged
です。その後で、text
を取得でき、他の API
を呼んで、Result
を受け取ります。そして、最後に最新の API
呼び出しのみ使いたいので、switchLatestInner
を呼び出します。searchResultsStream
を作成し、UITableView
にバインドします。これで、簡単に検索時の候補リストが作れます。
self.searchResultsStream =
searchBar.textChangedStream()
|> throttle(0.3)
|> distinctUntilChanged
|> map { text -> Stream<[Result]> in
return API.getSearchResultsStream(text)
}
|> switchLatestInner
この発表の中では、デモはスキップします。このレポジトリにデモがありますので、お好きなように見て、感想を教えてください。特に最後の例で見たように、変換や、フィルタ、結合、タイミング処理など数多くの機能があります。リアクティブプログラミングの重要な部分は、クラスよりも関数について学ぶことです。これらは基本的な機能で、他の言語でも同様のことができます。
ReactKit がどのように動くか? (24:10)
ReactKit の仕組みでは、map
や filter
のような機能を使って、ストリームを変化させることができました。ここで言う sourceStream
が、タイマーまたはユーザーからの入力になります。はじめはポーズの状態で待機しています。そして、Destination
(目的地)がこれらのストリームを監視するようになります。次にストリームを再開させるために一番近いところに問い合わせを行います。すると、アップストリームにリクエストが送られていき、ストリームが完全に動作し始めます。Source
が下流層に値を送り出し、その間で変更が加えられ、一番下流である Destination
が値を受け取ることになります。
もし、ここで他の Destination
があればどうなるでしょうか? 元となるストリームが、両方に値を送ることになります。ここの例では、map
ストリームが同じ値を filter
ストリームと flatMap
ストリームに送ります。同じ値を共有するようになり、必要であればそれぞれ別の変更を加えます。
Hot & Cold Observables (26:15)
では、Observable
の Hot と Cold の違いは何でしょうか? Observable
は RxJava や他のたくさんの言語で使われている用語です。Hot ストリームは、基本的にいつもアクティブで止まることはありません。いつも伝達できる状態にあります。それとは別に、Cold ストリームは止まっています。初めてそれを監視するときに、値が発信されます。しかし、Cold ストリームの面白いところは、二回目にそれを監視すると、同じ値は取得できません。それからは何も発信されません。ソースの裏側にあるものがクローンされ値が送られます。
Reactive プログラミングを行うとき、この二つをきちんと区別する必要があります。Rx.NET, RxJava, RxJS や他のライブラリでは、Observable
が Hot と Cold どちらの状態にもなります。ReactiveCocoa では、これらの概念は、Hot Observable として Signal
を Cold Observable として SignalProducer
が使われています。Reactive Extensions と比べると改善された API ですが、まだまだそれらは異なる概念です。ReactKit では一つの Stream
クラスが使われていて、デフォルトでは止まっています。そして、動き始めたときに他のストリームに情報が伝達されます。
この ReactKit の Stream
は Node.js の stream
のように様々な振る舞いをします。Node.js は状態を持っています。デフォルトでは止まっていて、再開すると、流れ出します。サブクラスは、close
するメソッドを持っています。ReactKit でできるような pause
, resume
, cancel
, close
などです。大きな違いは、Node.js の stream
は、独特なパイプを使うアプローチを取っています。アップストリームとダウンストリームがあり、pipe
というメソッドを使ってこの二つをつなぎ合わせます。また一方で、ReactKit はより関数的です。アップストリームがあり、ダウンストリームを作る必要はありません。map
のような関数を使うと、自動的に新しいストリームが作られます。
ReactKit では、ジェネリクス T
を持つ Stream
をソースとして持っています。これを変化させるときに、自動的に Stream<U>
が生成されます。これは、関数を使ったアプローチです。map
みたいな関数を使うのと、サブクラスを作りそれをインスタンス化し、pipe
でそれを結ぶのは、どちらの方が簡単でしょうか? 関数の方がサブクラスを作るよりも簡単だと思います。なので、ReactKit ではこの方法を採用することにしました。
まとめ (29:56)
最後に、ReactKit のまとめです。ReactKit は、Node.js の Stream のような状態を持ったモデルで、そのようにしているのは Hot と Cold の Observable を一つにまとめたのが理由です。そのことによって、この二つのクラスについて考える必要がなくなります。ReactKit は Promise から始まったアイディアで、Promise と ReactiveCocoa を一緒にしたような考えです。これは Reactive プログラミングの新しく面白いアプローチだと思います。
Q&A (30:47)
Q: Task
API には、時間の概念でリトライや途中で止めたりするようなものはありますか?
Yasuhiro: タイムアウトですか? 今のところそういった機能はありません。どのようにするか分かりません。SwiftTask は、Reactive プログラミングのスタートに過ぎません。Reactive プログラミングに調べてみると、もっといろんな機能があることが分かると思います。
Q: Stream
の中でなぜ NSError
を使うようにしたのですか?
Yasuhiro: 以前にも、プルリクエストでなぜジェネリクスのエラーを使うようにしないのかと聞かれたことがあります。このライブラリでは、Foundation の KVO を使っています。そのため、ジェネリクスを使ったエラーに変換するのがすこし面倒な作業になっています。NSError
を使うのが良かったのですが、しかし、ここは修正しようと思っています。
Q: 新しいバージョンの ReactiveCocoa では Swift のサポートがされています。なぜそれを使わなかったのですか?
Yasuhiro: 一番大きな違いは、Hot と Cold の二つの違いを覚える必要がないところにあります。ReactKit は、個人的な興味で作ったものです。ReactiveCocoa も使いたいと思っています。しかし、このアイデアもまた良いと思うので、開発は続けていきます。
About the content
This content has been published here with the express permission of the author.