iOS/Combine

[Combine] Applying Mathematical Operations on Elements - Operator 공부 4

Dev_Pingu 2022. 4. 16. 03:04
반응형

안녕하세요 Pingu입니다.🐧

 

지난 글에서는 Combine의 Operator 중 Reducing Elements 역할을 하는 Operator들을 알아봤었습니다. Upstream에서 받은 값들을 모아서 한 번에 Downastream으로 내려보내는 역할을 했었습니다.

 

이번 글에서는 이어서 Applying Mathematical Operations on Elements로 분류된 Operator에 대해 알아보도록 하겠습니다.

Applying Mathematical Operations on Elements

이름을 보면 Elements에 수학 연산을 적용한다라고 되어있는데요, Publishers에 이걸로 분류된 것에는 어떤 것들이 있는지부터 살펴보겠습니다.

  • Count
  • Comparison
  • TryComparison

공식문서에는 위와 같이 3개가 Applying Mathematical Operations on Elements로 분류되어있습니다.

 

이름만 봐서는 뭔가를 세고, 비교하는 역할을 할 거 같은데요, 이걸 활용해서 만든 Operator는 아래와 같습니다.

  • count()
  • max()
  • max(by:)
  • tryMax(by:)
  • min()
  • min(by:)
  • tryMin(by:)

공식문서에는 위와 같이 7개가 Applying Mathematical Operations on Elements Publishers로 만든 Operator로 분류되어있습니다. 이름들이 직관적이라 대충 봐도 뭐하는 녀석들인지 느껴지네요.

 

그럼 하나씩 알아볼게요!

Count

그럼 먼저 Count라는 Publisher를 알아볼게요.

 

정의를 보면 Upstream에서 받은 값의 개수를 Downstream으로 보내는 역할을 하는거 같습니다.

 

간단하네요!

Comparison

먼저 Comparison이라는 Publisher부터 알아보겠습니다.

 

정의를 보면 Upstream에서 받은 새로운 아이템이 이전의 것들보다 오름차순? 인 경우에만 Downstream에 내보낸다고 되어있는데.. 오름차순? 이 이상해서 구현부를 한 번 봤습니다.

 

public struct Comparison<Upstream> : Publisher where Upstream : Publisher {
	public typealias Output = Upstream.Output

    public typealias Failure = Upstream.Failure

    public let upstream: Upstream

    public let areInIncreasingOrder: (Upstream.Output, Upstream.Output) -> Bool

    public init(upstream: Upstream, areInIncreasingOrder: @escaping (Upstream.Output, Upstream.Output) -> Bool)

    public func receive<S>(subscriber: S) where S : Subscriber, Upstream.Failure == S.Failure, Upstream.Output == S.Input
}

코드를 보면 areInIncreasingOrder라는 게 있는데, 여기서 오름차순이라고 표현한 건 이 클로저에서 true를 반환하는 거라고 하네요.

그러니까 일반적인 오름차순이 아니고.. 어떤 것이던 저 클로저에서 true를 반환하면 그걸 Downstream에 내려보냅니다.

이름이 참 애매하네요 ㅋㅋㅋ

TryComparison

그럼 이번엔 Comparison에 Try가 붙은 TryComparison을 알아볼게요.

Try가 붙었으니 Comparison과 똑같으면서 에러를 내려보낼 수 있다는 차이만 있습니다.

 

그럼 이제 이 세 개의 Publisher를 활용해서 만들어진 Operator를 살펴보도록 하겠습니다.

count()

아까 알아본 Count Publisher를 활용해서 만들어진 Operator입니다.

정의를 보면 Upstream에서 받은 값의 개수를 세어주는 녀석이라고 합니다.

 

바로 간단하게 사용해볼게요.

let intPublisher = [1, 2, 3, 4, 5].publisher

intPublisher
    .count()
    .sink(receiveValue: { print($0) })

결과는 아래와 같습니다.

// count() 예제 코드
5

 

위 코드를 보면 intPublisher가 내보내는 값은 총 5개니까 출력도 5로 된 것을 볼 수 있어요.

 

끝입니다!

max()

아까 알아본 Comparison을 활용해서 만들어진 Operator입니다.

정의를 보면 Upstream이 finish 됐을 때 그때까지 받은 값 중에 가장 큰 값을 Downstream에 보내는 역할을 한다고 합니다.

당연한 말이지만 Output은 Comparable 프로토콜을 준수해야 합니다.

 

사용해보면 아래와 같아요.

let intPublisher = [5, 4, 10, 2, 1].publisher

intPublisher
    .max()
    .sink(receiveValue: { print($0) })

결과는 당연하게도 아래와 같습니다.

// max() 예제 코드
10

max(by:)

아까 본 max()와 다르게 Comparison의 areInIncreasingOrder, 즉 비교 로직을 직접 구현할 수 있는 operator입니다.

이건 Output이 Comparable 프로토콜을 준수하지 않을 때 사용하면 좋습니다.

 

바로 사용해보겠습니다.

struct Person {
    let name: String
    let age: Int
}

let personPublisher = [
    Person(name: "Pingu", age: 28),
    Person(name: "Pinga", age: 23),
    Person(name: "Roby", age: 5)
].publisher

personPublisher
    .max { $0.age < $1.age }
    .sink(receiveValue: { print($0) })

결과는 아래와 같습니다.

// max(by:) 예제 코드
Person(name: "Pingu", age: 28)

위 코드를 보면 Comparable을 준수하지 않는 Person이라는 구조체를 만들었습니다.

그리고 age가 가장 큰 값을 내보내도록 로직을 구현해줬어요.

그래서 결과는 위와 같이 age값이 가장 큰 Person 객체가 나오는 걸 볼 수 있습니다.

tryMax(by:)

이번에는 Try 시리즈입니다.

아까 사용해본 max에 에러도 내려보낼 수 있는 녀석입니다.

 

간단하게 사용해보면 아래와 같습니다.

struct NagativeNumberError: Error { }

let intPublisher = [5, 4, 10, -2, 1].publisher

intPublisher
    .tryMax { first, second in
        if second < 0 {
            throw NagativeNumberError()
        }
        return first < second
    }
    .sink(receiveCompletion: { print("completion: \($0)") },
          receiveValue: { print("value: \($0)") })

위와 같이 사용할 수 있고 결과는 아래와 같습니다.

// tryMax(by:) 예제 코드
completion: failure(__lldb_expr_14.(unknown context at $105e0c39c).(unknown context at $105e0c3a4).(unknown context at $105e0c3ac).NagativeNumberError())

위 코드는 기존 값과 비교할 새로운 값이 음수면 에러를 발생시킵니다.

 

만약에 기존 값에 음수인지 비교하는 로직으로 구현하면 어떻게 될까요?

struct NagativeNumberError: Error { }

let intPublisher = [5, 4, 10, -2, 1].publisher

intPublisher
    .tryMax { first, second in
//      if second < 0 {
        if first < 0 {
            throw NagativeNumberError()
        }
        return first < second
    }
    .sink(receiveCompletion: { print("completion: \($0)") },
          receiveValue: { print("value: \($0)") })

이렇게 하면 에러가 발생하지 않고 아래와 같은 결과를 보여줍니다.

// tryMax(by:) 예제 코드
value: 10
completion: finished

기존 값은 계속해서 양수니까 에러가 발생하지 않습니다.

min()

이젠 min 시리즈입니다.

얘는 max와 반대로 최솟값을 Downstream으로 내려보냅니다.

min()도 Output이 Comparable을 준수해야지 사용할 수 있습니다.

 

아까와 원리는 같으니까 빠르게 사용하고 넘어갈게요.

let intPublisher = [1, 2, 3, 4, 5].publisher

intPublisher
    .min()
    .sink(receiveValue: { print($0) })

결과는 아래와 같습니다.

// min() 예제 코드
1

min(by:)

아까 max(by:)와 마찬가지로 최소값을 결정하는 로직을 직접 구현할 수 있습니다.

max(by:)와 마찬가지로 Output이 Comparable을 준수하지 않을 때 유용합니다.

 

간단하게 사용해볼게요.

struct Person {
    let name: String
    let age: Int
}

let personPublisher = [
    Person(name: "Pingu", age: 28),
    Person(name: "Pinga", age: 23),
    Person(name: "Roby", age: 5)
].publisher

personPublisher
    .min { $0.age < $1.age }
    .sink(receiveValue: { print($0) })

위 코드의 결과는 아래와 같습니다.

// min(by:) 예제 코드
Person(name: "Roby", age: 5)

이번에도 Comparable을 준수하지 않는 Person이라는 구조체를 만들었습니다.

그리고 age가 가장 작은 값을 내보내도록 로직을 구현했어요.

결과는 위와 같이 age값이 가장 작은 Person 객체가 나온 것을 볼 수 있습니다.

 

근데 로직을 보면 "더 큰 값을 내보내는 로직 아닌가용?"이라고 의문이 들 수 있습니다.

물론 저도 의문이 들어서 좀 확인해봤습니다.

struct Person {
    let name: String
    let age: Int
}

let personPublisher = [
    Person(name: "Pingu", age: 28),
    Person(name: "Pinga", age: 23),
    Person(name: "Roby", age: 5)
].publisher

personPublisher
    .min {
        print("first: \($0), second: \($1)")
        return $0.age < $1.age
    }
    .sink(receiveValue: { print($0) })

이렇게 하면 결과는 아래와 같아요.

// min(by:) 예제 코드
first: Person(name: "Pinga", age: 23), second: Person(name: "Pingu", age: 28)
first: Person(name: "Roby", age: 5), second: Person(name: "Pinga", age: 23)
Person(name: "Roby", age: 5)

즉 비교를 할 때 max와 다르게 비교하는 두 개의 값의 위치가 반대입니다.

결과를 보면 첫 번째 비교에서 first는 Pingu이고 second는 Pinga여야 정상적인 순서인데, 이게 바뀌어있는 걸 볼 수 있어요.

 

그래서 로직이 저렇게 되어야 최솟값을 내보내는 로직이다!입니다.

헷갈릴 수 있으니 주의해야겠네요.

tryMin(by:)

뭐 min(by:)이랑 똑같은데 에러를 내려보낼 수 있다는 차이가 있습니다.

 

바로 사용해볼게요.

struct NameIsPinguError: Error { }

struct Person {
    let name: String
    let age: Int
}

let personPublisher = [
    Person(name: "Pinga", age: 23),
    Person(name: "Pingu", age: 28),
    Person(name: "Roby", age: 5)
].publisher

personPublisher
    .tryMin { first, second in
        if first.name == "Pingu" {
            throw NameIsPinguError()
        }
        return first.age < second.age
    }
    .sink(receiveCompletion: { print("completion: \($0)") },
          receiveValue: { print("completion: \($0)") })

결과는 아래와 같습니다.

// tryMin(by:) 예제 코드
completion: failure(__lldb_expr_39.(unknown context at $10662e7ec).(unknown context at $10662e89c).(unknown context at $10662e8a4).NameIsPinguError())

간단하죠?

 

이렇게 Combine의 Operator 중 Applying Mathematical Operations on Elements로 분류된 것들에 대해 알아봤습니다.

이전에 알아본 것들에 비해 비교적 간단하다는 느낌을 받았네요.

 

다음 글에서는 Applying Matching Criteria to Elements로 분류된 Operator에 대해 알아보도록 하겠습니다.

 

전체 코드는 여기에서 볼 수 있습니다.

 

감사합니다~!

반응형