Combine を使いこなしている人には、普通かもしれませんが、悩んだので自分メモ代わりに
Sponsor Link
環境&対象
- macOS Big Sur 11.3
- Xcode 12.5
- iOS 14.5
行いたい処理
[1,2,4,7,11,16,22,29] という数列が与えられた時に、その差分の数列を入手したいとします。欲しい差分数列とは、[1,2,3,4,5,6,7] です。
実装を考えてみる
for 文でループ処理
もちろん for 文を使ってループ処理することで できます。
func test_calcDiff() throws {
let intArray = [1,2,4,7,11,16,22,29]
var result:[Int] = []
for index in 1..<intArray.count {
let diff = intArray[index] - intArray[index-1]
result.append(diff)
}
XCTAssertEqual(result, [1,2,3,4,5,6,7])
}
Combine を使った 実現方法の検討
以下は、Combine を使ってどうやるかを考えてみます。
publisher
Combine を使うということで、配列から publisher を作って、オペレータを連結して解を得るということをしていきます。
配列は、以下のような形で publisher にすることができ、以降にオペレータを追加していくことができます。
func test_publisher1() throws {
let intArray = [1,2,4,7,11,16,22,29]
let result = intArray
// (1)
.publisher
// (2)
.sequence
XCTAssertEqual(result , [1,2,4,7,11,16,22,29])
}
- 配列を publisher 化しています
- publisher から、配列として取り出します(つけなくても、最終的な結果は同じです)
Publisher 化して、配列化しているだけなので、計算は何もしていません。
配列要素それぞれを +1 した配列
オペレータを適用することで、処理を追加することができます。
以下は、配列の要素それぞれを +1 するような処理を追加しています。
func test_publisher2() throws {
let intArray = [1,2,4,7,11,16,22,29]
let result = intArray
.publisher
// (1)
.map{$0+1}
.sequence
XCTAssertEqual(result, [2,3,5,8,12,17,23,30])
}
- 与えられた値に +1 したものを返すことで、各要素を +1 しています
上記の map オペレータは、その時の値のみが渡されてきています。渡された処理を +1 して返すことで、処理対象の各要素を +1 できます。ですが、その時の値のみが渡されるため、前後の値を参照することができません。
前後の情報を渡してくれるオペレータとしては、reduce や scan があります。
reduce オペレータと scan オペレータ
reduce と scan は、よく似たオペレータです。相違点としては、処理途中の値を都度 emit するか、最後まで処理を行なった時に最終結果を emit するかの違いです。
Apple の reduce についてのドキュメントは、こちら。
Apple の scan についてのドキュメントは、こちら。
Apple のドキュメントは、View Modifier 、Combine operator 共に、検索しづらいです。
operator については、Publishers という publisher (operator は、publisher の1種 です)のための名前空間が用意されているので、このページから必要な Publisher/Operator を探すのが便利です。
scan オペレータを検討する
差分数列を作るためには、要素ごとに、出力が必要となりますので、reduce/scan の出力タイミングを考えると reduce ではなく、scan が候補になります。
scan には 初期値と closure を与えることができます。実際に実行される時には、closure に 前回(closureが)返した値と今回の値が渡されます。
ここで、closure が その時の差分”のみ” を返してしまうと、次回処理時には、その差分のみ の情報しか渡されてこないため、その回での前回値との差分を計算することができません。
そこで、以下のように、差分値 と (前回)値 の 2つをもつ tuple を closure の返り値とすることで 前回差分値のみでなく 前回値も受け取ることができるようになります。
func test_scanWithTuple() throws {
let intArray = [1,2,3,6,10,15,21,28]
let result = intArray
.publisher
.scan((0,0), { (prevValues, value) in // (diff, value)
print("prevValue: \(prevValues), value: \(value)")
return (value - prevValues.1,value)
})
.sequence
print(result)
}
// print-out
prevValue: (0, 0), value: 1
prevValue: (1, 1), value: 2
prevValue: (1, 2), value: 4
prevValue: (2, 4), value: 7
prevValue: (3, 7), value: 11
prevValue: (4, 11), value: 16
prevValue: (5, 16), value: 22
prevValue: (6, 22), value: 29
[(1, 1), (1, 2), (2, 4), (3, 7), (4, 11), (5, 16), (6, 22), (7, 29)]
ここまでできれば あとは、Tuple の最初の要素を抜き出して数列を作ることで、目的の 差分の数列を得ることができます。
# 初期値に0を与えて計算しているので、不要であれば最初の要素を削除することが必要です。
func test_scanWithTuple() throws {
let intArray = [1,2,3,6,10,15,21,28]
let result = intArray
.publisher
.scan((0,0), { (prevValues, value) in // (diff, value)
print("prevValue: \(prevValues), value: \(value)")
return (value - prevValues.1,value)
})
.map{$0.0}
.dropFirst()
.sequence
print(result)
XCTAssertEqual(Array(result), [1,1,3,4,5,6,7])
}
// print-out
prevValue: (0, 0), value: 1
prevValue: (1, 1), value: 2
prevValue: (1, 2), value: 4
prevValue: (2, 4), value: 7
prevValue: (3, 7), value: 11
prevValue: (4, 11), value: 16
prevValue: (5, 16), value: 22
prevValue: (6, 22), value: 29
DropFirstSequence>(_base: [1, 1, 2, 3, 4, 5, 6, 7], _limit: 1)
# dropFirst を適用したため result が ArraySlice となっています。比較のために XCTAssertEqual の中で、Array 化してます。
まとめ:Combine の活用
- .publisher を使うと、配列を Publisher にできる
- .scan, .reduce を使うと 前回処理時の情報を取得できる
説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。
Sponsor Link