[Swift][Combine] Combine で差分数列を作る

Combine を使いこなす一例を紹介します。

Combine を使いこなしている人には、普通かもしれませんが、悩んだので自分メモ代わりに

環境&対象

以下の環境で動作確認を行なっています。

  • 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..

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,3,6,10,15,21,28])
    }
コード解説
  1. 配列を publisher 化しています
  2. 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 したものを返すことで、各要素を +1 しています

上記の map オペレータは、その時の値のみが渡されてきています。渡された処理を +1 して返すことで、処理対象の各要素を +1 できます。ですが、その時の値のみが渡されるため、前後の値を参照することができません。

前後の情報を渡してくれるオペレータとしては、reduce や scan があります。

reduce オペレータと scan オペレータ

reduce と scan は、よく似たオペレータです。相違点としては、処理途中の値を都度 emit するか、最後まで処理を行なった時に最終結果を emit するかの違いです。

Apple の reduce についてのドキュメントは、こちら

Apple の scan についてのドキュメントは、こちら

MEMO
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: 3
prevValue: (1, 3), value: 6
prevValue: (3, 6), value: 10
prevValue: (4, 10), value: 15
prevValue: (5, 15), value: 21
prevValue: (6, 21), value: 28
[(1, 1), (1, 2), (1, 3), (3, 6), (4, 10), (5, 15), (6, 21), (7, 28)]

ここまでできれば あとは、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: 3
prevValue: (1, 3), value: 6
prevValue: (3, 6), value: 10
prevValue: (4, 10), value: 15
prevValue: (5, 15), value: 21
prevValue: (6, 21), value: 28
DropFirstSequence>(_base: [1, 1, 1, 3, 4, 5, 6, 7], _limit: 1)

# dropFirst を適用したため result が ArraySlice となっています。比較のために XCTAssertEqual の中で、Array 化してます。

まとめ:Combine の活用

Combine の活用
  • .publisher を使うと、配列を Publisher にできる
  • .scan, .reduce を使うと 前回処理時の情報を取得できる

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です