콘텐츠로 건너뛰기
Home » [Swift] fast enumeration을 지원하는 Sequence타입

[Swift] fast enumeration을 지원하는 Sequence타입

Sequence Type in Swift

Swift는 Objective-C에서 fast-enumeration이라 불리던 집합타입 내 원소 개체들을 순회하는 반복문을 지원한다. (이미 많은 언어들이 이러한 반복문 개념을 지원하고 있다.) Python의 그것과 매우 유사하게 Swift의 for..in 구문은 부적으로 시퀀스, 제너레이터라는 개념을 사용하고 있다.
Swift가 기본적으로 제공하는 Array, Dictionary는 기본적으로 for..in 구문예 적용이 가능하다. 그리고 이들은 내부적으로 SequenceType이라는 프로토콜을 따르고 있는데, 이는 다음과 같이 정의되어 있다.

여러 블로그에서 Sequence라는 프로토콜 명을 쓰는데, Xcode 6.1을 기준으로 명명이 변경되어 현재는 SequenceType이 맞다.

protocol SequenceType: _Sequence_Type {
    typealias Generator: GeneratorType
    func generate() -> Generator
}

이유는 모르겠으나, SequenceType_Sequence_Type이라는 프로토콜을 상속하고 있는데, 이 정의 역시 완전히 동일하다.

protocol _Sequence_Type: _SequenceType {
    typealias Generator: GeneratorType
    func generate() -> Generator
}
protocol _SequenceType {}

다시 _Sequence_Type이 상속하고 있는 _SequenceType은 그 정의가 비어있다.

protocol _SequenceType {}

어쨌든 다시 살펴보면 이 프로토콜은 두 가지 기능을 정의하고 있다.

  1. Generator 타입 : 제너레이터타입이다. 이 타입은 GeneratorType 프로토콜을 따르는 임의의 타입이다.
  2. generate() 함수. 위에서 정의한 제너레이터 객체를 리턴한다.

제너레이터 객체는 어떤 시퀀스(연속열)의 원소들을 차례차례 꺼내주는 역할을 한다. 그리고 더 이상 꺼내올 원소가 없으면 nil을 리턴한다.

protocol GeneratorType {
    typealias Element
    mutating func next() -> Element?
}

만약 어떤 커스텀 타입이 SequenceType 프로토콜을 따른다면 for .. in 구문을 아래와 같이 쓸 수 있다.

for anItem in aSequenceType {
    doSomethingWith(anItem)
}

이는 결국 아래와 동일하게 동작한다.

var aGenerator = aSequenceType.generate()
while let anItem = aGenerator.next() {
    doSomethingWith(anItem)
}

for .. in 문을 지원하는 TodoItemList

for .. in 문을 쓸 수 있는 커스텀 타입을 만들어보자. 이는 결국 SequenceType 프로토콜을 따라야하고, 따라서 제너레이터 타입을 추가적으로 정의할 필요가 있다. 간단한 todo 리스트를 만들고 이 리스트가 fast enumeration을 지원하도록 해보자.
먼저 데이터모델 타입은 다음과 같이 정의한다.

struct TodoItem {
    var title: String = "untitled"
    var done: Bool = false
}

리스트는 단순히 각 아이템의 집합을 갖고 있는 컨테이너로 정의한다.

struct TodoItemList {
    var items = Array<TodoItem>()
    var addItem(item) {
        items.append(item)
    }
}

시퀀스타입 프로토콜을 정의하기 위해 GeneratorType을 정의한다. 기본적으로 명시적인 조건에 가장 충실하게 작성해보았다.

프로토콜내에서 typealias를 정의한 것을 연관타입(associated type)이라고 하는데 많은 경우 Swift의 타입시스템이 이를 추론할 수 있는 경우는 이를 생략해도 무방하다. 선언적으로 명시해야 하는 경우에는 Generic 타입으로 선언하는 것으로도 갈음할 수 있다.

struct TodoItemGenerator : GeneratorType {
    typealias Element = TodoItem
    var items: [Element]
    var i: Int = 0
    mutating func next() -> Element? {
        if i == items.count {
            return nil
        } else {
            return items[i++]
        }
    }
}

제너레이터의 구조는 간단하고 구현도 단순하다. 이제 이를 적용하여 TodoList 타입을 시퀀스타입으로 만들어보자.

struct TodoItemList : SequenceType { //Version 2
    var items = Array<TodoItem>()
    var addItem(item) {
        items.append(item)
    }
    typealias Generator: GeneratorType = TodoItemGenerator
    func generate() -> Generator {
        return TodoItemGenerator(items:items)
    }
}

보다 깔끔하게

위 구현을 보다 깔끔하고 이해하기 쉽게 다시 작성해보자.

struct TodoItemGenerator : GeneratorType {
    var items: [TodoItem]
    var i: Int = 0
    mutating func next() -> TodoItem? {
        if items.isEmpty {
            return nil
        } else {
            return items.removeAtIndex(0)
        }
    }
}

먼저 제너레이터 타입이다. 이 타입은 오직 TodoItem 전용으로 만든 제너레이터이기 때문에 타입시스템은 Element에 해당하는 타입이 TodoItem임을 추론할 수 있다. 그리고 원소의 배열 자체는 사본을 넘겨받기 때문에 맨 앞의 원소를 하나씩 빼서 리턴해주면 된다.

struct TodoItemList : SequenceType { // Version 3
    var items = Array<TodoItem>()
    var addItem(item) {
        items.append(item)
    }
    func generate() -> TodoItemGenerator {
        return TodoItemGenerator(items:items)
    }
}

시퀀스 타입의 경우에도 마찬가지다. 제너레이터 타입은 함수 generate()에서 명시되었으므로 별도의 타입을 명시해줄 필요가 없다.

제네릭 제너레이터

어떤 커스텀 시퀀스타입에 대해 매번 제너레이터를 작성하는 것은 불편하므로 제네릭타입으로 재작성해보도록 하자.

struct GenericGenerator<T>: GeneratorType {
    var list = [T]()
    mutating next() -> T? {
        if list.isEmpty {
            return nil
        } else {
            return list.removeAtIndex(0)
        }
    }
}

그러면 TodoList는 다음과 같이 수정할 수 있다.

struct TodoItemList : SequenceType { // Version 3
    var items = Array<TodoItem>()
    var addItem(item) {
        items.append(item)
    }
    func generate() -> GeneratorType<TodoItem> {
        return GeneratorType<TodoItem>(list:items)
    }
}

리피터

제너레이터는 어떤 리스트를 한번 순회할 수도 있지만 계속해서 되돌이할 수도 있다. 다음은 어떤 리스트를 받아서 계속 반복하는 리피터 타입이다.

struct RepeatGenerator<T>: GeneratorType {
    var items: Array<T> = []
    var i = 0
    mutating func next()->T? {
        let n = items[i]
        i = (i + 1) % items.count
        return n
    }
}

간단하고, 시퀀스타입 정의 역시 별거 없다.

struct Repeater<T> : SequenceType {
    var items: Array<T>
    func generate() -> RepeatGenerator<T> {
        return RepeatGenerator<T>(items:items, i:0)
    }
}

어떤 타입이든 배열만 있다면 리피터를 만들 수 있다.

var i = 0
var r = Repeater(items:[1,2,3])
for k in r {
    println(k)
    i += 1
    if i > 10 {
        break
    }
}

피보나치 수열 생성기

연속열이 특정한 규칙이 있다면 점화식을 사용해서 얼마든지 제너레이터를 만들 수 있고, 제너레이터가 있다면 시퀀스타입은 쉽게 생성가능하다. 다음은 피보나치 수열을 생성하는 제너레이터이다.

struct FibonacciGenerator : GeneratorType {
    var (a:Int, b:Int) = (0, 1)
    mutating func next() -> Int? {
        (b, a) = (a+b, b)
        return b
    }
}
struct FibonacciSequence : SequenceType {
    func generate() -> FibonacciGenerator {
        return FibonacciGenerator()
    }
}

그외에 순열생성기라든지(알고리듬이 어려워서 다음 기회에…) 뭐 다양한 타입들을 만들 수 있을 것이다.