(Swift) Array 완전정복 – 02. Sequence 프로토콜

Sequence

시퀀스(Sequence)는 직역하면 연속열이 될 수 있으며, 문자 그대로 개개의 원소들을 순서대로 하나씩 순회할 수 있는 타입을 의미한다. (Swift 기본 타입에 대해서는 사실상 모든 집합 타입이 이에 해당한다.) 시퀀스는 사실 Swift 문법과 밀접한 관련이 있는데, 바로 for - in 구문에 사용된다는 점이다.1

let numbers = 2...7
for n in numbers {
  print(n)
}
// 2, 3, 4, 5, 6, 7

 

요건

실제 Sequence 프로토콜에는 이것 저것 정의되어 있는데 공식문서에서는 makeIterator() 만 구현해주면 나머지는 거의 공짜로 얻을 수 있다고 한다. 문제는 makeIteratorIteratorProtocol을 만족해야 하는 타입인데, 바로 연속열에서 매 스텝에서 다음번 원소를 리턴해주는 역할을 하는 시퀀스의 엔진과 같다.

커스텀 시퀀스 타입 만들어보기

이를 정석으로 구현해보면 다음과 같은 모양이 된다.

///: 카운트 다운용 시퀀스 타입을 정의해본다.
struct CountDown: Sequence {
    var value: Int

    struct CountDownIterator: IteratorProtocol {
        var value: Int
        mutating func next() -> Int ? {
            if value < 0 { return nil }
            defer{ value -= 1}
            return value
        }
    }

    func makeIterator() -> CountDownIterator {
        return CountDownIterator(value: self.value)
    }
}

for i in CountDown(value: 5) {
  print(i)
}
// 5, 4, 3, 2, 1, 0

사실 nested type으로 내부에 이터레이터를 또 정의하는 것은 조금 번거롭다. 따라서 이마저도 자기 스스로가 IteratorProtocol을 만족한다면 되기 때문에 실질적으로는 next()라는 메소드 하나만 정의해주면 된다. 2

따라서 카운트 다운은 간단하게 다음과 같이 구현할 수도 있다.

struct SimpleCountDown: Sequence, IteratorProtocol {
    var value: Int
    mutating func next() -> Int? {
      if value < 0 { return nil }
      value -= 1
      return value + 1
    }
}

for i in SimpleCountDown(5) { print(i) }
// 5, 4, 3, 2, 1, 0 각 라인에 출력

같은 값을 계속 반복하는 무한 시퀀스도 다음과 같이 반복할 수 있다.

struct Repeater<T>: Sequence, IteratorProtocol {
   let value: T
   mutating func next() -> T? { return value }
}

///: **주의!!!** Repeater를 그냥 for in 문에 넣으면 무한 루프가 된다.
for i in Repeater(value: 3).prefix(10) { print(3) }
// 3, 3, 3,  ... 3 을 10번 출력한다.  

시퀀스는 그 자체의 본성(?)에 의해 내부에서 값을 순서대로 하나씩 꺼내어 순회할 수 있기 때문에 contains(_:)와 같은 동작을 다음과 같이 구현해볼 수 있다.

for x in someSequence {
  if x == valueToFind { 
    print("found")
    break
  }
}

이는 내부 원소가 Equatable이기만 하면 똑같은 모양으로 어떤 시퀀스에서나 적용할 수 있으므로 아예 Sequence 타입의 표준 구현이 되었다.

if someSequence.contains(x) {
  print("found")
} else {
  print("not found")
}

앞선 예제에서 만들었던 스택 역시, 시퀀스로 만들 수 있다. 맨 위에 있는 요소로부터 하나씩 스택을 꺼내면서 순회하는 것이 가능할 거라 충분히 예상할 수 있기 때문이다.

extension Stack : Sequence, IteratorProtocol {
    mutating func next() -> Element? {
        if isEmpty { return nil }
        return pop()
    }
}

시퀀스의 제공 기능

시퀀스가 공짜로 제공해주는 메소드들은 다음과 같은 것들이 있다.3

  • contains(where:) , 만약 원소가 Equatable이면 contains(_:) 도 가능하며 특정 조건/원소의 포함여부를 알려준다.
  • prefix(), prefix(_:), suffix(), suffix(_:):맨 앞/뒤에서 한개 혹은 n개 원소를 얻을 수 있다. (Element, Subsequence<Elemenet>)
  • dropFirst(), dropLast() , dropFirst(_:), dropLast(_:)prefix, suffix의 반대
  • enumerated()for (index, value) in ...에 쓰이는 그것.
  • filter(_:), map(_:), reduce(_:_:) – 맵 필터 리듀스.
  • flatMap(_:) – 원소가 시퀀스나 옵셔널일 때.
  • split(whereSeperator:) 혹은 Equatable 한 원소에 대해서는 split(sperator:)로 시퀀스의 배열을 만들 수 있다.
  • first(where:), 앞쪽에서 원소를 찾기
  • starts(with:by:), start(with:) : 특정 원소나 시퀀스와 앞이 일치하는지
  • joined(), joined(_:)
  • sorted(by:), sorted(), revered()

take(while:), drop(while:)이 없는데, Swift4에서 추가될 것 같다. 물론 구현은 쉬우니 직접 확장해보는 것은 어떨까?

함수형 코딩에서 가장 중요한 맵, 필터, 리듀스4가 공짜로 지원됨은 물론 정렬하고 뒤집고, 자르고, 합치는 등의 작업이 모두 지원된다. 이 쯤이면 Sequence 타입에 대해서 알아둘만한 가치가 있지 않을까?

반복 액세스시 주의점

위와 같이 구현하는 시퀀스는 대부분 for - in 을 반복 적용할 때 문제가 생기지 않는다 5 하지만 일부 시퀀스의 경우에 순회작업이 desctructive한 경우도 있다. 이 경우에는 순회를 마치거나 혹은 중간에 빠져나왔을 때, 반복 순회를 할 때 어떤 동작을 할지 예측할 수 없다. 시퀀스 프로토콜의 요건은 단지 특정 시점에서 다음 지점의 원소를 꺼내쓰는 것일 뿐, 다음 순회에서 재시작할 지, 이어서 진행할지에 대한 부분은 제한 조건으로 보증하지 않기 때문이다.

위에서 스택을 시퀀스로 만들어 본 바 있다. 하지만 이 스택은 destructive sequence가 아니다. 왜냐하면 매 순회시에 만들어지는 이터레이터는 자신의 사본이기 때문이다. 따라서 값 타입으로 생성된 스택은 몇 번이고 for ... in 할 수 있다. 하지만 스택이 클래스로 정의되었다면, 매 순회시에 만들어지는 이터레이터는 자기 자신을 참조하기 때문에 순회가 끝나면 스택이 텅 비게 된다. 이 경우에는 반복적으로 for in 루프를 탈 경우 두 번째부터는 순회할 원소가 없기 때문에 루프 내부가 실행되지 않을 것이다.

지금까지 Sequence에 대해 몇가지 내용들을 살펴보았다. 핵심은 Sequence는 그저 앞에서부터 순차적으로 원소를 액세스해 나가는 구조라는 점과, 그에 따라 함수형 언어의 리스트 구조와 닮아있으며, 실제로 하스켈 등에서 제공하는 기본 리스트 관련 함수들이 디폴트로 제공된다는 점이다. 그리고 이러한 디폴트 제공 메소드들의 기능으로 사실 상 Array 조작과 관련된 대부분의 기능을 커버하게 된다.

하지만 Array는 실제로 시퀀스의 능력 밖의 기능들도 가지고 있다. 시퀀스는 각 원소를 연속적으로 만나서 끝까지 순회한다는 개념이기 때문에 인덱스를 통한 랜덤액세스를 지원하지 않는다. 그리고 뒤에서 앞으로 역순으로 조회하는 개념도 제공하지 않으며, 내부의 삽입이나 치환역시 제공하지 않는다.

다음글에서는 시퀀스외의 기능을 제공하는 Collection 프로토콜에 대해서 살펴보도록 하겠다.


목차

  1. Array – Array 타입, 생성과 조작
  2. Array – Sequence 프로토콜
  3. Array – Collection 프로토콜
  4. Array – ArraySlice
  5. Array – NSArray

참고자료


  1. 말 그대로 for ... in 구문은 연속열에서 각 원소를 하나씩 순회하는 동작이며, 더이상 C 스타일의 for(;;)문이 지원되지 않으면서 Swift의 기본 for문이 되었다. 
  2. for - in 문은 실질적으로 시퀀스의 이터레이터를 받아서, next()nil을 리턴할 때까지 반복하는데, 시퀀스 스스로가 이터레이터 프로토콜을 만족한다면 makeIterator()는 자기 자신의 사본을 리턴한다. 
  3. 전체 메소드는 swiftdoc.org공식레퍼런스를 참고하자. 여기는 흔히 쓰이거나 기본적인 내용만 언급했다. 특정 메소드는 원소가 Comparable 이나 Equatable 일 때만 동작할 수 있다. 
  4. 맵, 필터, 리듀스는 함수형 언어의 코어 데이터구조인 List를 위한 연산인데, 리스트 역시 앞쪽의 원소부터 순차적으로 확인할 수 있으므로 Sequence와 같은 행동 양식을 보인다. 그외 함수형 언어의 중급(?) 연산인 scan 등의 연산도 있을 수 있는데, 조만간 Swift 에 추가될 예정인 듯. 
  5. 값 타입의 이터레이터를 쓰기 때문에 매번 makeIterator()할 때마다 새 이터레이터가 생기며, 시퀀스 원본의 값이 손상되지는 않는다.