ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Chapter 7: Sequence Operators
    Raywenderlich/Combine: Asynchronous Programming 2020. 8. 9. 04:27

    Version

    Swift 5.3, iOS 14, Xcode 12

     

    Combine이 제공해야하는 대부분의 연산자(operators)에 대해 배웠지만, 자세히 살펴 보아야할 한 가지 범주(category)가 더 있다. 바로 시퀀스 연산자(Sequence Operators)이다.

    시퀀스(sequence) 연산자(operators)는 publishers가 시퀀스(sequences) 자체일 뿐이라는 것을 깨달을(realize) 때 가장 이해하기 쉽다. 시퀀스(sequence) 연산자(operators)는 배열(array)이나 집합(set)과 같은 publisher의 값 컬렉션(collection)과 함께 작동한다. 배열(array)이나 집합(set)도 유한(finite) 시퀀스(sequences)일 뿐이다.

    다른 범주(categories)의 연산자(operators)가 그러하듯이, 시퀀스(sequence) 연산자(operators)는 대부분의 시퀀스(sequence)를 개별(individual) 값(values)이 아닌 전체(whole)로 처리(deal)한다.

    이 범주(category)에 속하는 대다수 연산자(operators)는 Swift 표준(standard) 라이브러리(library)의 해당 연산자(counterparts)와 거의 동일한 이름과 특성(behaviors)을 가지고 있다.

     

    Getting started

    projects/Starter.playground에서 이 장(chapter)의 시작(starter)playground를 찾을 수 있다. playground에 코드를 추가(add)하고 실행(run)하여, 다양한 시퀀스(sequence) 연산자(operators)가 publisher를 어떻게 조작(manipulate)하는지 확인한다. print 연산자(operator)를 사용하여 모든 publishing 이벤트(events)를 기록한다.

     

    Finding values

    첫 섹션(section)은 다른 기준(criteria)에 따라 publisher가 내보낸(emits) 특정(specific) 값(values)을 찾는 연산자(operators)로 구성된다. 이는 Swift 표준(standard) 라이브러리(library)의 collection 메서드(methods)와 유사하다.

     

    min

    min 연산자(operator)를 사용하여 publisher가 내보낸(emitted) 최소값(the minimum value)을 찾을 수 있다. publisher가 .finished 완료(completion) 이벤트(event)를 보낼 때까지 기다려야 한다(greedy). publisher가 완료(completes)되면 연산자(operator)는 최소값(the minimum value)만 내보낸다(emitted):

     

    다음 예제를 playground에 추가하여, min의 사용 방법을 확인해 본다:

    example(of: "min") {
        let publisher = [1, -50, 246, 0].publisher
        //4개의 다른 숫자를 내보내는 publisher를 생성한다.
    
        publisher
            .print("publisher")
            .min() //min 연산자를 사용하여 publisher가 내보낸 값 중 최소 값을 찾는다.
            .sink(receiveValue: { print("Lowest value is \($0)") }) //출력
            .store(in: &subscriptions) //저장
    }

    playground를 실행(run)하면, 콘솔(console)에서 다음과 같은 출력(output)을 확인할 수 있다:

    ——— Example of: min ———
    publisher: receive subscription: ([1, -50, 246, 0]) publisher: request unlimited
    publisher: receive value: (1)
    publisher: receive value: (-50)
    publisher: receive value: (246)
    publisher: receive value: (0)
    publisher: receive finished
    Lowest value is -50

    보다시피 publisher가 모든 값(values)을 내보내고(emits) 완료(finishes)한 다음, min이 최소값(minimum)을 찾아 다운 스트림(downstream)으로 전송하여 sink에서 출력한다.

    Combine이 최소값(minimum)을 찾아낼 수 있는 이유는 숫자 값(numeric value)이 Comparable 프로토콜(protocol)을 준수하기 때문이다. Comparable을 준수하는 유형(types)을 내보내(emit)는 publishers에서는 인수(arguments)없이 min()을 직접 사용할 수 있다.

    값(values)이 Comparable을 준수(conform)하지 않을 때에는, min(by:) 연산자(operator)를 사용하여 자체 비교(comparator) 클로저(closure)를 제공할 수 있다.

    publisher가 내보낸(emits) 여러 Data 유형에서 가장 작은 값을 찾는 다음 예제를 고려해본다.

    playground에 다음 코드를 추가한다:

    example(of: "min non-Comparable") {
        let publisher = ["12345", "ab", "hello world"]
            .compactMap { $0.data(using: .utf8) } // [Data]
            .publisher // Publisher<Data, Never>
        //다양한 문자열에서 생성된 세 개의 Data 객체를 내보내는 publisher를 생성한다.
    
        publisher
            .print("publisher")
            .min(by: { $0.count < $1.count })
            //Data 유형은 Comparable을 구현하지 않았기 때문에, min(by:) 연산자를 사용한다.
            //여기서는 바이트 수가 가장 적은 Data 객체를 찾는다.
            .sink(receiveValue: { data in
                let string = String(data: data, encoding: .utf8)!
                //Data를 String으로 변환한다.
                print("Smallest data is \(string), \(data.count) bytes")
            })
            .store(in: &subscriptions) //저장
    }

    playground를 실행(run)하면, 콘솔(console)에서 다음을 확인할 수 있다:

    ——— Example of: min non-Comparable ———
    publisher: receive subscription: ([5 bytes, 2 bytes, 11 bytes]) publisher: request unlimited
    publisher: receive value: (5 bytes)
    publisher: receive value: (2 bytes)
    publisher: receive value: (11 bytes)
    publisher: receive finished
    Smallest data is ab, 2 bytes

    이전(previous) 예제와 마찬가지로 publisher는 모든 Data 객체(objects)를 내보내고(emits) 완료(finishes)한 다음, min(by:)으로 가장 작은 바이트(byte) 크기(size)의 데이터를 찾아 내보내고(emits) sink가 이를 출력한다.

     

    max

    짐작할 수 있듯이, max는 publisher가 내보낸(emitted) 최대 값(the maximum value)을 찾아낸다는 것만 제외하면 min과 똑같이 작동한다.

     

    playground에 다음 코드를 추가하여 해당 예제를 확인해 본다:

    example(of: "max") {
        let publisher = ["A", "F", "Z", "E"].publisher
        //4개의 다른 문자를 내보내는 publisher를 생성한다.
    
        publisher
            .print("publisher")
            .max() //max 연산자를 사용하여 최대 값을 찾는다.
            .sink(receiveValue: { print("Highest value is \($0)") }) //출력
            .store(in: &subscriptions) //저장
    }

    playground를 실행(run)한다. 다음과 같은 결과(output)를 확인할 수 있다:

    ——— Example of: max ———
    publisher: receive subscription: (["A", "F", "Z", "E"])
    publisher: request unlimited
    publisher: receive value: (A)
    publisher: receive value: (F)
    publisher: receive value: (Z)
    publisher: receive value: (E)
    publisher: receive finished
    Highest value is Z

    min과 같이 max는 최대 값(the maximum value)을 결정(determines)하기 전에 업 스트림(upstream) publisher가 모든 값을 내보낼(emitting) 때까지 기다려야(wait) 한다(greedy). 이 경우, 해당 값(value)은 Z이다.

    Note: min과 똑같이 max에는 Comparable이 아닌 값(values)에서 최대 값(the maximum value)을 결정(determine)하기 위한 max(by:) 연산자(operator)가 있다.

     

    first

    minmax 연산자(operators)는 알려지지 않은(unknown) 인덱스(index)에서 published 값(value)을 찾는(finding) 것을 처리(deal with)했지만, 이 섹션(section)의 다른 연산자(operators)들은 특정(specific) 위치에서 방출(emitted)된 값(values)을 찾는(finding) 작업을 처리한다.

    first 연산자(operator)는 처음(first) 방출된(emitted) 값을 통과(through)시킨 다음 완료(completes)한다는 점을 제외하면, Swift 컬렉션(collections)에서의 first 속성(property)과 유사하다. lazy이기 때문에 업 스트림(upstream) publisher가 완료(finish)될 때까지 기다리지 않고, 내보낸(emitted) 첫 번째(first) 값(value)을 수신(receives)하면 구독(subscription)을 취소(cancel)한다.

     

    위의 예제를 playground에 추가한다:

    example(of: "first") {
        let publisher = ["A", "B", "C"].publisher
        //3개의 다른 문자를 내보내는 publisher를 생성한다.
    
        publisher
            .print("publisher")
            .first() //첫 번째로 내보낸 값만 통과시킨다.
            .sink(receiveValue: { print("First value is \($0)") }) //출력
            .store(in: &subscriptions) //저장
    }

    playground를 실행(run)하고, 콘솔(console)을 확인한다:

    ——— Example of: first ———
    publisher: receive subscription: (["A", "B", "C"])
    publisher: request unlimited
    publisher: receive value: (A)
    publisher: receive cancel
    First value is A

    first()가 첫 번째 값(value)을 통과(through)시키자마자, 업 스트림(upstream) publisher에 대한 구독(subscription)을 즉시(immediately) 취소(cancels)한다.

    보다 세분화(granular)된 제어(control)를 원한다면 first(where:)를 사용할 수도 있다. Swift 표준(standard) 라이브러리(library)에 있는 것과 마찬가지로 제공된(provided) 조건(predicate)과 일치(matches)하는 첫 번째(first) 값(value)을 내보낸다(emit).

    다음 예제를 playground에 추가한다:

    example(of: "first(where:)") {
        let publisher = ["J", "O", "H", "N"].publisher
        //3개의 문자를 내보내는 publisher를 생성한다.
    
        publisher
            .print("publisher")
            .first(where: {"Hello World".contains($0) }) //Hello World에 포함된 첫 번째 문자를 찾는다.
            .sink(receiveValue: { print("First match is \($0)") }) //출력
            .store(in: &subscriptions) //저장
    }

    playground를 실행(run)하면 다음과 같은 출력(output)을 확인할 수 있다:

    ——— Example of: first(where:) ———
    publisher: receive subscription: (["J", "O", "H", "N"])
    publisher: request unlimited
    publisher: receive value: (J)
    publisher: receive value: (O)
    publisher: receive value: (H)
    publisher: receive cancel
    First match is H

    위의 예제에서 연산자(operator)는 첫 번째 일치점(match)을 찾을 때까지, Hello World에 내보낸(emitted) 문자가 포함(contains)되어 있는지 확인한다. 여기서는 H가 되고, 이를 찾으면 구독(subscription)을 취소(cancels)하고 sink에서 출력(print out)할 수 있도록 문자를 내보낸다(emits).

     

    last

    maxmin의 반대(opposite)인 것처럼, lastfirst의 반대(opposite)이다.

    last는 publisher가 내보낸(emits) 마지막 값(the last value)을 방출(emits)한다는 점을 제외(except)하면 first와 똑같이 작동한다. 이는 업 스트림(upstream) publisher가 완료(finish)될 때까지 기다려야(wait) 한다는 것을 의미한다(greedy):

     

    이 예제를 playground에 추가한다:

    example(of: "last") {
        let publisher = ["A", "B", "C"].publisher
        //3개의 문자를 내보내는 publisher를 생성한다.
    
        publisher
            .print("publisher")
            .last() //마지막 값만 내보낸다.
            .sink(receiveValue: { print("Last value is \($0)") }) //출력
            .store(in: &subscriptions) //저장
    }

    playground를 실행(run)하면 다음과 같은 결과(output)를 확인할 수 있다:

    ——— Example of: last ———
    publisher: receive subscription: (["A", "B", "C"])
    publisher: request unlimited
    publisher: receive value: (A)
    publisher: receive value: (B)
    publisher: receive value: (C)
    publisher: receive finished
    Last value is C

    last는 업 스트림(upstream) publisher가 .finished 완료(completion) 이벤트(event)를 보낼(send) 때까지 기다린다. 마지막(last)으로 내보낸(emitted) 값(value)을 다운 스트림(downstream)으로 보내 sink에서 출력한다.

    Note: first와 마찬가지로, last에는 지정된 조건(predicate)과 일치하는 publisher가 내보낸(emitted) 마지막 값(the last value)을 방출(emitted)하는 last(where:) 연산자(operator)가 있다.

     

    output(at:)

    이 섹션(section)의 마지막 두 연산자(operators)는 Swift 표준(standard) 라이브러리(library)에 대응하는(counterparts) 연산자(operators)가 없다. output 연산자(operators)는 지정된(specified) 인덱스(indices)에서 업 스트림(upstream) publisher가 값을 내보낸(emitted) 경우에만 값(values)을 통과시킨다.

    먼저, 지정된(specified) 인덱스(index)에서 방출된(emitted) 값(value)만 내보내(emits)는 output(at:)이 있다:

     

    playground에 다음 코드를 추가하여, 이 예제를 확인해 본다:

    example(of: "output(at:)") {
        let publisher = ["A", "B", "C"].publisher
        //3개의 문자를 내보내는 publisher를 생성한다.
    
        publisher
            .print("publisher")
            .output(at: 1) //인덱스 1에서 방출된 값. 즉, 두 번째 값만 통과시킨다.
            .sink(receiveValue: { print("Value at index 1 is \($0)") }) //출력
            .store(in: &subscriptions) //저장
    }

    playground에서 예제를 실행(run)하고 콘솔(console)을 확인(peek at)한다:

    ——— Example of: output(at:) ———
    publisher: receive subscription: (["A", "B", "C"])
    publisher: request unlimited
    publisher: receive value: (A)
    publisher: request max: (1) (synchronous)
    publisher: receive value: (B)
    Value at index 1 is B
    publisher: receive cancel

    여기서 출력은 인덱스 1의 값(value)이 B임을 나타낸다. 그러나 추가적으로 흥미로운 사실을 알 수 있습니다. 연산자(operator)는 특정(specific) 인덱스(index)에서만 항목(item)을 찾는다는 것을 알고 있으므로, 내보낸(emitted) 값(value)마다 한 개의 값(value)을 더 요구(demands)한다. 이는 특정(specific) 연산자(operator)의 구현(implementation) 세부 사항(detail)이지만, Apple이 자체 내장된(built-in) Combine 연산자(operators)를 설계(designs)하는 방법에 대한 흥미로운 통찰력(insight)을 제공한다.

     

    output(in:)

    output 연산자(operator)의 오버로딩(overload)으로 이 섹션(section)을 마무리(wrap up)한다.

    output(at:)은 지정된(specified) 인덱스(index)에서 방출(emitted)된 단일 값(single value)을 내보내지만, output(in:)은 지정된 인덱스(indices) 범위(range) 내에 있는 값들을 내보낸다:

     

    다음 예제를 playground에 추가하여, 이를 확인해 본다:

    example(of: "output(in:)") {
        let publisher = ["A", "B", "C", "D", "E"].publisher
        //5개의 서로 다른 문자를 내보내는 publisher를 생성한다.
    
        publisher
            .output(in: 1...3) //인덱스 1 ~ 3 까지에서 방출된 값만 통과시킨다.
            .sink(receiveCompletion: { print($0) },
                  receiveValue: { print("Value in range: \($0)") }) //출력
            .store(in: &subscriptions) //저장
    }

    이 예제의 출력을 확인하기 위해, playground를 실행(run)한다:

    ——— Example of: output(in:) ———
    Value in range: B
    Value in range: C
    Value in range: D
    finished

    연산자(operator)는 인덱스 모음(collection)이 아닌, 인덱스 범위(range of indices) 내의 개별 값(individual values)을 내보낸다(emits).

    연산자(operator)는 각각(respectively) 인덱스(indices) 1, 2, 3에 있는 B, C, D 값(values)을 출력한다. 그런 다음 범위(range) 내의 모든 항목(items)이 방출(emitted)되었으므로, 작업을 완료(complete)하는 데 필요한 모든 항목을 받는(receives) 즉시 구독(subscription)을 취소(cancels)한다.

     

    Querying the publisher

    다음 연산자(operators)도 publisher가 내보낸(emitted) 전체 값 집합(the entire set of values)을 처리(deal with)하지만, 내보낸(emits) 특정(specific) 값(value)을 산출(produce)하지는 않는다. 대신 이런 연산자들(operators)은 publisher 전체(whole)에 대한 어떤 질의(query)를 나타내는(representing) 다른 값(value)을 내보낸다(emit). 이에 대한 좋은 예는 count 연산자(operator)이다.

     

    count

    count 연산자(operator)는 publisher가 .finished 완료(completion) 이벤트(event)를 보내면(sends), 업 스트림(upstream) publisher가 내보낸(emitted) 값의 개수를 나타내는 단일 숫자(single number)를 방출(emit)한다:

     

    이 예제를 확인하기 위해 다음 코드를 추가한다:

    example(of: "count") {
        let publisher = ["A", "B", "C"].publisher
        //3개의 문자를 내보내는 publisher를 생성한다.
    
        publisher
            .print("publisher")
            .count() //업 스트림 publisher가 내보내는 값의 개수를 나타내는 단일 값을 방출한다.
            .sink(receiveValue: { print("I have \($0) items") }) //출력
            .store(in: &subscriptions) //저장
    }

    playground를 실행(run)하고 콘솔(console)을 확인한다. 다음과 같은 결과를 볼 수 있다:

    ——— Example of: count ———
    publisher: receive subscription: (["A", "B", "C"])
    publisher: request unlimited
    publisher: receive value: (A)
    publisher: receive value: (B)
    publisher: receive value: (C)
    publisher: receive finished
    I have 3 items

    예상대로 값(value) 3은 업 스트림(upstream) publisher가 .finished 완료(completion) 이벤트(event)를 보낸(sends) 경우에만 출력된다.

     

    contains

    또 다른 유용한 연산자(operator)는 contains이다. Swift 표준(standard) 라이브러리(library)에서 해당 연산자(counterpart)를 한 번 이상 사용해 봤을 것이다.

    contains 연산자(operator)는 지정된(specified) 값(value)이 업 스트림(upstream) publisher에서 방출된(emitted) 경우 true를 내보내고 구독(subscription)을 취소(cancel)하며, 방출된(emitted) 값(values)이 지정된(specified) 값과 같지 않으면 false를 내보낸다:

     

    다음을 playground에 추가하여 contains을 사용해 본다:

    example(of: "contains") {
        let publisher = ["A", "B", "C", "D", "E"].publisher
        //5개의 서로 문자를 내보내는 publisher를 생성한다.
        let letter = "C"
        //포함 여부를 확인할 문자
    
        publisher
            .print("publisher")
            .contains(letter) //해당 문자를 업 스트림 publisher에서 내보냈는지 확인한다.
            .sink(receiveValue: { contains in //contains 야부에 따라 적절한 메시지를 출력한다.
                print(contains ? "Publisher emitted \(letter)!"
                               : "Publisher never emitted \(letter)!")
            })
            .store(in: &subscriptions) //저장
    }

    playground를 실행(run)하고 콘솔(console)을 확인한다:

    ——— Example of: contains ———
    publisher: receive subscription: (["A", "B", "C", "D", "E"])
    publisher: request unlimited
    publisher: receive value: (A)
    publisher: receive value: (B)
    publisher: receive value: (C)
    publisher: receive cancel
    Publisher emitted C!

    publisher가 C를 내보냈다(emitted)는 메시지(message)를 받는다. 작업을 수행하는 데 필요한 만큼의 업 스트림(upstream) 값(values)만 소비(consumes)하므로 contains는 lazy하다. C를 찾으면 구독(subscription)을 취소(cancels)하고 더 이상 값을 생성(produce)하지 않는다.

    다른 변형을 시도해 본다. 아래 행(line)을

    let letter = "C"

    다음으로 바꾼다:

    let letter = "F"

    그리고 playground를 실행(run)한다. 다음과 같은 결과를 볼 수 있을 것이다:

    ——— Example of: contains ———
    publisher: receive subscription: (["A", "B", "C", "D", "E"])
    publisher: request unlimited
    publisher: receive value: (A)
    publisher: receive value: (B)
    publisher: receive value: (C)
    publisher: receive value: (D)
    publisher: receive value: (E)
    publisher: receive finished
    Publisher never emitted F!

    이 경우 contains은 publisher가 F를 내보낼(publisher) 때까지 대기(waits for)한다. 그러나 publisher는 F를 내보내지(emitting) 않고 완료(finishes)되므로 containsfalse를 내보내고(emits) 적절한 메시지(message)를 출력한다.

    마지막으로, 제공(provide)하는 조건(predicate)에 일치(match for)하는 항목을 찾거나, Comparable을 준수(conform)하지 않는 방출(emitted) 값(value)의 존재를 확인해야할 때도 있다. 이러한 특정 사례에 대해서 contains(where:)을 사용할 수 있다.

    다음 예제를 playground에 추가한다:

    example(of: "contains(where:)") {
        struct Person {
            let id: Int
            let name: String
        }
        //id와 name으로 구조체 Person을 정의한다.
    
        let people = [
            (456, "Scott Gardner"),
            (123, "Shai Mishali"),
            (777, "Marin Todorov"),
            (214, "Florent Pillet")
        ]
            .map(Person.init) //생성
            .publisher
        //Person의 다른 4개의 인스턴스를 내보내는 publisher를 생성한다.
    
        people
            .contains(where: { $0.id == 800 }) //id가 800인 Person이 있는지 확인한다.
            .sink(receiveValue: { contains in
                print(contains ? "Criteria matches!"
                               : "Couldn't find a match for the criteria")
                //내보낸 결과에 따라 적절한 메시지를 출력한다.
            })
            .store(in: &subscriptions) //저장
    }

    playground를 실행(run)하면 다음과 같은 결과(output)를 볼 수 있다:

    ——— Example of: contains(where:) ———
    Couldn't find a match for the criteria

    내보낸(emitted) Person 중 id800인 객체가 없기 때문에 예상대로 일치(matches)하는 항목을 찾지 못했다.

    다음으로, contains(where:)의 아래 구현을

    .contains(where: { $0.id == 800 })

    다음과 같이 변경한다:

    .contains(where: { $0.id == 800 || $0.name == "Marin Todorov" }

    playground를 다시 실행(run)해서 콘솔(console)을 살펴본다:

    ——— Example of: contains(where:) ———
    Criteria matches!

    이번에는 Marin이 실제로 목록(list)에 있는 Person 중 하나이기 때문에, 조건(predicate)과 일치(matching)하는 값을 찾았다.

     

    allSatisfy

    이제 두 개의 연산자만 남았다. 두 연산자 모두 Swift 표준(standard) 라이브러리(library)에 대응하는(counterpart) 컬렉션(collection) 메서드(methods)가 있다.

    먼저 allSatisfy로 시작한다. 이는 클로저(closure) 조건자(predicate)를 취하고, 업 스트림(upstream) publisher가 내보낸(emits) 모든 값(values)이 해당 조건(predicate)과 일치(match)하는지 여부를 나타내는 Boolean 값을 방출(emits)한다.

    따라서 업 스트림(upstream) publisher가 .finished 완료(completion) 이벤트(event)를 내보낼(emits) 때까지 기다린다(greedy):

     

    다음 예제를 playground에 추가하여 확인해 본다:

    example(of: "allSatisfy") {
        let publisher = stride(from: 0, to: 5, by: 2).publisher
        //0 ~ 5 사이의 숫자를 2단계씩 내보내는 publisher를 생성한다(0, 2, 4).
    
        publisher
            .print("publisher")
            .allSatisfy { $0 % 2 == 0 } //'모든' 방출된 값이 짝수인지 확인한다.
            .sink(receiveValue: { allEven in
                print(allEven ? "All numbers are even"
                              : "Something is odd...")
                //내보낸 결과에 따라 적절한 메시지를 출력한다.
            })
            .store(in: &subscriptions) //저장
    }

    코드를 실행(run)하고, 콘솔(console)의 출력(output)을 확인한다:

    ——— Example of: allSatisfy ———
    publisher: receive subscription: (Sequence)
    publisher: request unlimited
    publisher: receive value: (0)
    publisher: receive value: (2)
    publisher: receive value: (4)
    publisher: receive finished
    All numbers are even

    모든 값이 실제로 짝수(even)이므로, 업 스트림(upstream) publisher가 .finished 완료(completion)를 보내고, 연산자(operator)가 true를 방출(emits)한 후 적절한(appropriate) 메시지(message)를 출력한다.

    그러나 하나의 값(a single value)이라도 조건(the predicate condition)을 통과하지 못하면, 연산자(operator)는 즉시 false를 내보내고(emit) 구독(subscription)을 취소(cancel)한다.

    아래 행(line)을

    let publisher = stride(from: 0, to: 5, by: 2).publisher
    //0 ~ 5 사이의 숫자를 2단계씩 내보내는 publisher를 생성한다(0, 2, 4).

    다음과 같이 바꾼다:

    let publisher = stride(from: 0, to: 5, by: 1).publisher
    //0 ~ 5 사이의 숫자를 1단계씩 내보내는 publisher를 생성한다(0, 1, 2, 3, 4).

    단순히 2대신 1 로, 05stride 단계(step)를 변경했다. 다시 한 번 playground를 실행(run)하고 콘솔(console)을 살펴본다:

    ——— Example of: allSatisfy ———
    publisher: receive subscription: (Sequence) publisher: request unlimited
    publisher: receive value: (0)
    publisher: receive value: (1)
    publisher: receive cancel
    Something is odd...

    이 경우 1이 방출(emitted)되는 즉시 조건(predicate)을 더 이상 만족시키지 않으므로, allSatisfyfalse를 내보내고(emits) 구독(subscription)을 취소(cancels)한다.

     

    reduce

    이 장(chapter)의 마지막 연산자(operator)는 reduce 이다.

    reduce 연산자(operator)는 이 장(chapter)에서 다룬 나머지 연산자들과 약간 다르다. 특정(specific) 값(value)을 찾거나 publisher 전체를 질의(query)하지 않는다. 대신 업 스트림(upstream) publisher가 내보낸 값(emissions)을 기반으로 새로운 값을 반복적으로(iteratively) 축적(accumulate)할 수 있다.

    처음에는 헷갈릴(confusing) 수 있지만, 금방 이해할 수 있을 것이다. 가장 쉬운 방법은 도표(diagram)를 확인하는 것이다:

     

    Combine의 reduce 연산자(operator)는 Swift 표준(standard) 라이브러리(library)의 reduce(_:_)reduce(into:_:) 처럼(counterparts) 작동한다. 이는 시드 값(seed value)과 누산(accumulator) 클로저(closure)를 제공(provide)할 수 있다. 이 클로저(closure)는 시드 값부터 시작한 누적된(accumulated) 값(value)과 현재(current) 값(value)을 받는다(receives). 해당 클로저(closure)에서 새로운 누적(accumulated) 값(value)을 반환(return)한다. 연산자(operator)가 .finished 완료(completion) 이벤트(event)를 수신(receives)하면 최종 누적(accumulated) 값(value)을 내보낸다(emits).

    위의 도표(diagram)의 경우, 다음과 같이 생각할 수 있다:

    Seed value is 0
    Receives 1, 0 + 1 = 1
    Receives 3, 1 + 3 = 4
    Receives 7, 4 + 7 = 11
    Emits 11

    이 연산자(operator)를 더 잘 이해하기 위해 간단한 예제를 확인해 본다. playground에 다음을 추가한다:

    example(of: "reduce") {
        let publisher = ["Hel", "lo", " ", "Wor", "ld", "!"].publisher
        //6개의 문자열을 내보내는 publisher를 생성한다.
    
        publisher
            .print("publisher")
            .reduce("") { accumulator, value in
                //seed와 함께 reduce를 사용한다. 내보낸 값을 추가하여 최종 문자열 결과를 만든다.
                accumulator + value
            }
            .sink(receiveValue: { print("Reduced into: \($0)") })
            .store(in: &subscriptions) //저장
    
    }

    playground를 실행(run)하고, 콘솔(console)의 출력(output)을 살펴본다:

    ——— Example of: reduce ———
    publisher: receive subscription: (["Hel", "lo", " ", "Wor", "ld", "!"])
    publisher: request unlimited
    publisher: receive value: (Hel)
    publisher: receive value: (lo)
    publisher: receive value: ( )
    publisher: receive value: (Wor)
    publisher: receive value: (ld)
    publisher: receive value: (!)
    publisher: receive finished
    Reduced into: Hello World!

    누적된(accumulated) 결과(result)인 Hello World!를 확인한다. 업 스트림(upstream) publisher가 .finished 완료(completion) 이벤트(event)를 보낸(sent) 후에만 출력된다.

    reduce의 두 번째 인수(argument)는 어떤 유형의 두 값(values)을 취하여 동일한 유형의 값(value)을 반환(returns)하는 클로저(closure)이다. Swift의 +는 해당 시그니처(signature)와 일치하는 함수(function)이다.

    따라서 깔끔하게 위의 구문을 줄일 수 있다. 아래 코드를

    .reduce("") { accumulator, value in
        accumulator + value
    }

    다음과 같이 바꾼다:

    .reduce("", +)

    다시 playground를 실행(run)해보면, 이전과 똑같이 작동한다.

    Note: 3장(chapter), "Transforming Operators"에서 scan에 대해 배웠기 때문에 이 연산자(operator)가 친숙하게 느껴질 수도 있다. scanreduce는 기능(functionality)이 동일하지만, 주요 차이점은 scan이 모든 방출(emits)된 값에 대해 누적된(accumulated) 값(value)을 내보내는(emits) 반면, reduce는 업 스트림(upstream) publisher가 .finished 완료(completion) 이벤트(event)를 보내면(sends) 누적된 단일 값(single accumulated value)을 내보낸다(emits)는 것이다. 위의 예제에서 reduce대신 scan을 사용하여 직접 시도해 본다.

     

    Key points

    • publishers는 컬렉션(collections)과 시퀀스(sequences)처럼 값(values)을 생성(produce)하므로, 실제로 시퀀스(sequences)이다.
    • minmax를 사용하여, publisher가 각각(respectively) 내보낸(emitted) 최소값(minimum) 또는 최대값(maximum)을 내보낼 수 있다.
    • first, last, output(at:)은 특정 인덱스(index)에서 내보낸(emitted) 값(value)을 찾으려는 경우 유용하다. 인덱스 범위(range of indices) 내에서 방출된(emitted) 값(values)을 찾으려면 output(in:)을 사용한다.
    • first(where:)last(where:)은 각각은 통과(through)시켜야하는 값(values)을 결정(determine)하기 위해 조건(predicate)을 사용한다.
    • count, contains, allSatisfy와 같은 연산자(operators)는 publisher가 방출한(emitted) 값(values)을 내보내지(emit) 않는다. 오히려 방출 된(emitted) 값(values)에 따라, 다른(different) 값(value)을 내보낸다.
    • contains(where:)는 publisher가 지정된 값을 포함(contains)하는지 확인하기(determine) 위해 조건(predicate)을 사용한다.
    • 방출된(emitted) 값(values)을 단일 값(single value)으로 누적(accumulate)하려면 reduce를 사용한다.
Designed by Tistory.