【Swift】【Combine入門】AnyPublisher の Output を Never にした場合の挙動


# はじめに

Combine で処理を連結するときに、`AnyPublisher` の `Output` に空の `Void` を返して、何も返り値がないことを暗に示していたのですが、それよりは `.ignoreOutput()` して `Output` を `Never` にしたほうが意図が伝わるのでは？ と思い、それで実装したところ思ったように動かなかったため、挙動を整理しました。


環境：Xcode 12.5.1

# 結論

先に結論から述べます。

- `AnyPublisher` の `Output` が `Never` になった段階でストリームは流れなくなる
- 何も返り値がない場合の `AnyPublisher` の `Output` は `Void` を返して、`Never` にしたい箇所で `.ignoreOutput()` とつけて `Never` にしてあげるのが良さそう

# 実験その1

`CurrentValueSubject<Int, Never>` の `number` が 0 か負の値かチェックするという適当な処理を例にして実験します。

```swift
import Combine
import Foundation

enum TestError: Error {
    case zeroError
    case minusError
}

var cancellables: Set<AnyCancellable> = []
let number = CurrentValueSubject<Int, Never>(0)

func start() {
       number
        .dropFirst()
        .setFailureType(to: TestError.self)
        .flatMap { _ -> AnyPublisher<Never, TestError> in
            print("checkZero")
            return checkZero()
        }
        .flatMap { _ -> AnyPublisher<Never, TestError> in
            print("checkMinus") // 上のflatMapの段階でNeverになるのでここにストリームは流れてこない
            return checkMinus()
        }
        .sink { completion in
            switch completion {
            case .finished:
                print("finished")
            case let .failure(error):
                print("error: \(error)")
            }
            print("completion.")
        } receiveValue: { hoge in
            print("receiveValue: \(hoge)")
        }
        .store(in: &cancellables)
}

func checkZero() -> AnyPublisher<Never, TestError> {
    Future<Void, TestError> { promise in
        let numberValue = number.value
        print("now: \(numberValue)")
        guard number.value != 0 else {
            promise(.failure(TestError.zeroError))
            return
        }
        promise(.success(()))
    }
    .ignoreOutput()
    .eraseToAnyPublisher()
}

func checkMinus() -> AnyPublisher<Never, TestError> {
    Future<Void, TestError> { promise in
        let numberValue = number.value
        print("now: \(numberValue)")
        guard numberValue > 0 else {
            promise(.failure(TestError.minusError))
            return
        }
        promise(.success(()))
    }
    .ignoreOutput()
    .eraseToAnyPublisher()
}

start()

number.value = 1
number.value = -1 // 本当はここでminusErrorになってほしいがcheckMinusに到達しない
number.value = 0

// 出力
// checkZero
// now: 1
// checkZero
// now: -1
// checkZero
// now: 0
// error: zeroError
// completion.
```

このように `checkMinus()`まで処理が到達していないことがわかります。
つまり、前段の ` checkZero()` で止まっていることがわかります。

- ポイント
    - `AnyPublisher` 返す関数側で `Never` を返すようにしている
    - `AnyPublisher` の `Output` が `Never` になった段階でストリームは流れなくなる（本当は流れてほしい）
        - → 途中の `flatMap` で `Never` にするとストリームはそこで止まる

# 実験その2

```swift
import Combine
import Foundation

enum TestError: Error {
    case zeroError
    case minusError
}

var cancellables: Set<AnyCancellable> = []
let number = CurrentValueSubject<Int, Never>(0)

func start() {
       number
        .dropFirst()
        .setFailureType(to: TestError.self)
        .flatMap { _ -> AnyPublisher<Void, TestError> in
            print("checkZero")
            return checkZero()
        }
        .flatMap { _ -> AnyPublisher<Never, TestError> in // 最後のflatMapでNeverにする
            print("checkMinus")
            return checkMinus() // AnyPublisher<Void, TestError>
                .ignoreOutput() // Void を Never に変換
                .eraseToAnyPublisher() // 型合わせ
        }
        .sink { completion in
            switch completion {
            case .finished:
                print("finished")
            case let .failure(error):
                print("error: \(error)")
            }
            print("completion.")
        } receiveValue: { hoge in
            print("receiveValue: \(hoge)")
        }
        .store(in: &cancellables)
}

func checkZero() -> AnyPublisher<Void, TestError> { // Voidに変更
    Future<Void, TestError> { promise in
        let numberValue = number.value
        print("now: \(numberValue)")
        guard number.value != 0 else {
            promise(.failure(TestError.zeroError))
            return
        }
        promise(.success(()))
    }
//    .ignoreOutput()
    .eraseToAnyPublisher()
}

func checkMinus() -> AnyPublisher<Void, TestError> { // Voidに変更
    Future<Void, TestError> { promise in
        let numberValue = number.value
        print("now: \(numberValue)")
        guard numberValue > 0 else {
            promise(.failure(TestError.minusError))
            return
        }
        promise(.success(()))
    }
//    .ignoreOutput()
    .eraseToAnyPublisher()
}

start()

number.value = 1
number.value = -1
number.value = 0

// 出力
// checkZero
// now: 1
// checkMinus
// now: 1
// checkZero
// now: -1
// checkMinus
// now: -1
// error: minusError
// completion.
// checkZero
// now: 0
```

今回は `checkZero()` の `flatMap` では、`Never` としていないため、ストリームが流れて、`checkMinus()` まで処理が流れていることがわかります。

また、最後の `AnyPublisher` 返す関数側で `Never` であれば、`sink()` の `receiveValue` までストリームがながれていないこともわかります（もしながれていれば、`"receiveValue: \(hoge)"` が出力されるはず）。

これは、 `Never` になった段階でストリームは流れなくなる証拠でもあります。

- ポイント
    - `AnyPublisher` 返す関数側で `Never` とはせずに、`flatMap` の中で必要に応じて、 `Never` に変換する
    - これなら、ストリーム側でコントールが可能になる
      - → 何も返り値がない場合の `AnyPublisher` の `Output` は `Void` を返して、`Never` にしたい箇所で `.ignoreOutput()` とつけてあげるのが良さそう

# まとめ

- `AnyPublisher` の `Output` が `Never` になった段階でストリームは流れなくなる
- 何も返り値がない場合の `AnyPublisher` の `Output` は `Void` を返して、`Never` にしたい箇所で `.ignoreOutput()` とつけて `Never` にしてあげるのが良さそう