Swift - 순차적 타입(Sequence Type) 만들기

Sequence Type의 대표적인 예는 배열(Array)이 있겠다. 하지만 정확히 말하자면 배열 처럼 고정인덱스를 가진 타입이 아니라 그냥 순차적인 리스트 정도가 딱 어울리는 표현이다. 다르게 표현하자면 이터레이션(Iteration), 즉 나열 가능한 타입이다. 어쨌든 이런 타입을 만드는데 필요한 SequenceType 프로토콜에 대해 알아보자.

참고로 이번 글은 아래와 같은 코드로 동작하는 것이 목표이다.
for value in MyRange(start: 10, end: 20) {
    print(value)
}
이렇게 실행시켰을 때 콘솔에 10에서 20까지 찍혀야 한다.

GeneratorType

시작부터 엉뚱한 이름의 프로토콜이 등장했는데 사실 SequenceType 은 이 GeneratorType 프로토콜을 따르는 인스턴스를 리턴시키는 것이 목표인 프로토콜이라 어쩔 수 없다.

GeneratorType은 단순히 .next() 라는 메소드를 구현해야 하는 프로토콜이다. 실제 선언부는 레퍼런스 매뉴얼을 참고하고 여기서는 어떤 식으로 구현하는지를 예로 살펴보자.
struct MyRangeGenerator: GeneratorType {
    let end: Int
    var current: Int
    
    // 1
    typealias Element = Int
    
    init(start: Int, end: Int) {
        self.current = start
        self.end = end
    }
    
    // 2
    mutating func next() -> MyRangeGenerator.Element? {
        if self.current > end { return nil }
        let currentValue = self.current
        self.current++
        
        return currentValue
    }
}
위의 코드에서 1, 2로 주석표기한 코드가 핵심이다:
  1. Element 라는 것을 Int로 재정의 하고 있다. 이는 이 제너레이터가 취급하는 데이터가 Int 타입이라는 의미이다. 만약 다른 타입을 쓰고 싶다면 원하는 타입을 Element로 별칭을 지어주면 된다.
  2. next() 라는 메소드를 구현하고 있는데 이 녀석이 Element 타입의 데이터를 리턴한다. 이름을 보면 알겠지만 Iteration의 next에 해당하는 녀석이다. 즉 값을 리턴하고 다음 차례의 값을 준비하는 것이 목적인 메소드다. 더이상 돌려줄 값이 없으면 nil을 리턴한다.
이 제너레이터는 next()가 호출될 때 마다 start로 넘겨진 값에서 end 값 까지 1씩 더해가며 돌려주는 역활이다.

SequenceType

이제 핵심에 해당하는 SequenceType이 나왔다. 말 그대로 순차적 나열(Sequence)이 가능한 타입이라는 의미의 프로토콜이다.

이 프로토콜은 generate() 라는 메소드를 구현하는 것이 최종 목표인데 앞서 본 GeneratorType을 구현한 인스턴스를 리턴시키는 것임을 유추할 수 있다.
struct MyRange: SequenceType {
    let start: Int
    let end: Int
    
    // 1
    typealias Generator = MyRangeGenerator
    
    init(start: Int, end: Int) {
        self.start = start
        self.end = end
    }
    
    // 2
    func generate() -> MyRange.Generator {
        return MyRangeGenerator(start: self.start, end: self.end)
    }
}
역시 1, 2로 표기한 코드가 핵심이다:
  1. Generator 라는 별칭은 사용될 제너레이터의 타입을 의미한다. 즉 여기서도 원하는 타입을 Generator 라는 별칭을 붙여주면 된다.
  2. generate() 메소드의 역활은 실제 제너레이터를 리턴하는 것이다.
이제 이렇게 구현해 두고 앞서 이야기한 목적에 해당하는 코드를 실행시켜 보면 아마 원하는 값이 출력될 것이다.
for value in MyRange(start: 10, end: 20) {
    print(value)
}
// 콘솔에는 10, 11, 12 .... , 20 까지 숫자가 찍힌다.

정리

왜 SequenceType과 GeneratorType이 분리되어 있는지는 아무래도 for-in 구문의 내부 구현에 달린 것 같다. 뭐 그러려니 하고 넘어가는게 좋겠다. 내부 구현까지 들여다 볼 용기는 없으니까.

어쨌거나
  • 나열가능한 타입은 SequenceType 프로토콜을 따르면 구현이 가능하다.
  • 그런데 SequenceType 은 GeneratorType 프로토콜을 따르는 인스턴스를 리턴하는게 목표다.
  • GeneratorType은 next() 라는 메소드를 이용하 순차적인 값을 리턴해 주는 것이 목적이다.
정도로 정리가 가능하다.

그런데 좀 더 생략이 가능하다

마지막에 갑자기 이상한 내용이 나와서 미안하다. 사실 위의 제너레이터로 분리된 타입을 하나로 합쳐서 구현하는 것도 가능하다.

위의 MyRangeGenerator를 없애버리고 MyRange 구현을 아래와 같이 바꾸는 것이 가능하다.
struct MyRange: SequenceType {
    let start: Int
    let end: Int
    
    init(start: Int, end: Int) {
        self.start = start
        self.end = end
    }
    
    func generate() -> AnyGenerator<Int> {
        var current: Int = self.start
        
        return anyGenerator {
            if current > self.end { return nil }
            let value = current
            current++
            
            return value
        }
    }
}
SequenceType의 내부에 GeneratorType의 next() 메소드 구현부를 그대로 옮겨놨다. 그 대신 AnyGenerator 라는 특수한 타입의 제너레이터를 리턴하도록 바꾸었다.

결론적으로 구조체(혹은 클래스) 구현은 하나지만 앞서 본 제너레이터 역활을 대신하는 클로져를 anyGenerator 라는 함수를 통해 리턴하고 있다.

어떤 구조가 더 좋을지는 본인이 직접 판단할 문제다. 단지 제너레이터가 복잡하다면 차라리 구현을 분리하는게 더 좋을 것 같다는 생각이다.

[관련글] 스위프트(Swift) 가이드
[관련글] Swift - 구조체(Structure) 훑어보기
[관련글] Swift - 클래스(Class) 훑어보기
[관련글] Swift - 프로토콜(Protocols)

댓글

이 블로그의 인기 게시물

버전(Version)을 제대로 이해하기

소수점 제거 함수 삼총사 ceil(), floor(), round()