(Swift) 시퀀스와 관련된 Swift 표준 함수들

Swift 기본함수 중에는 Sequence를 만드는 함수들이 제법 있다. 이러한 함수 중에서 가장 많이 사용할 법한 함수로 우선 stride()를 들 수 있다. 이 함수는 파라미터가 다른 두 가지 버전이 있는데 하나는 stride(from:to:by:) 이고 다른 하나는 stride(from:through:by:)이다. 첫번째 버전은 to: 뒤의 경계를 포함하지 않으며, 두 번째 버전은 ClosedRange처럼 뒤쪽 경계값을 포함한다.

이는 주어진 범위 내에서 step을 달리하여 건너뛰는 값들을 하나씩 얻을 수 있게 한다.

for a in stride(from:10.0, to:20.0, by:0.2) { print(a) }

위 코드는 10.0 ~ 19.8 까지의 값에 대해 0.2 간격으로 출력한다. 혹은 거꾸로 내려가는 값들을 표현하기에도 좋다.

for a in stride(from:5.0, through:0.0, by:-0.5) { print(a) }
// 5.0, 4.5, 4.0, ... , 0.5, 0.0

이는 정수가 아닌 실수값1 등에 대해서 특정 범위에서 반복문을 만들 때, 혹은 특정 범위에서 임의의 step을 이용해서 시퀀스를 만들 때 사용한다.

이 함수들의 리턴형은 Sequence를 따르는 StrideTo, StrideThrough 제네릭 구조체 타입이다.2

다음으로는 repeatElement(_:count:) 가 있다. 이는 특정 원소가 지정된 개수만큼 반복되는 시퀀스를 생성한다. 이 때의 리턴타입은 제네릭 구조체인 Repeated이며 이는 Collection이다.

Repeated : 모든 원소가 동일한 콜렉션3

다음은 이름 그대로 시퀀스를 만들 것 처럼 생긴 sequence() 함수인데 여기에도 두 가지 버전이 존재한다. 그 중 하나는 sequence(first:next:)인데 시작값과, 어떤 값으로부터 다음 값을 만드는 함수를 이용해서 무한 수열을 생성할 수 있다. 다음 원소는 현재 원소 값을 기준으로 만들 수 있으므로, 등차수열이나 등비수열을 만들 때 유용하게 쓸 수 있다4

let _3n_plus1 = sequence(first:1){ $0 + 3 }
for x in _3n_plus1.prefix(10) { print(x) }
// 1, 4, 7, 10, 13, 16, 19, 22, 25, 28

두 번째 함수는 비슷하게 생겼는데, 값이 아닌 계산에 필요한 팩터들을 하나의 상태에 저장해두고 이를 계속 변경하면서 다음항을 계산할 수 있다. 일례로 피보나치 수열을 다음과 같이 계산할 수 있다. 5

var s = (a: 0, b: 1)
let fib = sequence(state: s) { (state: inout (a: Int, b: Int)) -> Int? in 
    defer {state = (state.b, state.a + state.b)}
    return state.a
}
for f in fib.prefix(10) { print(f) }
// 0, 1, 1, 2, 3, 5, 8, 13, 21, 34

위에서 보이는 두 번째 버전의 타입 시그니처를 눈여겨봐두자.

func sequence<State, Element>(state: State, next: @escaping (inout State) -> Element?) -> UnfoldSequence<Element>

여기서 리턴타입은 UnfoldSequence이다. 이는 원소들이 특정 원소나 변경가능한 상태값에 대해서 클로저를 반복 적용하여 생성되는 시퀀스로 설명된다.6 이 수열의 수학적 특성 상 모든 원소는 한 번에 계산되어 생성되는 것이 아니라 매번 계산해야 하기 때문이다.

시퀀스의 각 원소들은 느긋하게 계산되며, 시퀀스의 길이는 무한수열이 될 수 있다. 6

하지만 중요한 것은 이 수열이 항상 ‘느긋한’ 것은 아니라는 것이다. 예를 들어 다음 코드는 무한루프가 되며 제대로 실행되지 않는다.

let s = sequence(first:1, next:{ $0 + 3})
for x in s.map{ $0 + 1 }.prefix(10) {
  print(x)
}

s.map{ $0 + 1 }s의 모든 원소에 대해 eagerly 하게 적용되기 때문에 무한수열을 만들기 때문이다. 이러한 무한 수열을 취급할 때는 수열 자체를 lazy하게 만들거나 한정자를 반드시 적용한다.

let s = sequence(first:1, next:{ $0 + 3}).prefix(1000).map{ $0 + 1 }
for x in s { print(x) }

이 코드에서도 sequence()의 결과물은 느긋한 수열이지만 prefix를 하는 시점에 eagerly하게 계산되기 때문에 1000개의 값이 모두 구해진 시퀀스가 된다. 보다 나은 방법은 Sequence.lazy 프로퍼티를 이용하는 것이다.

let s = sequence(first:1){ $0 + 3 }.lazy.map{ $0 + 1 }.prefix(1000)
for x in s { print(x) }

이 코드에서는 lazy를 통해서 아예 LazySequence를 만들었고, 이 타입은 맵, 필터에 대해서 클로저 자체를 캡쳐하는 느긋한 수열이 만들어진다. 따라서 실제 for - in루프를 돌 때 각 시퀀스의 원소가 구해지고 그것이 다시 map을 거치는 과정을 수행한다. 따라서 수열을 생성/준비하는 시간을 들일 필요가 없으므로 안전하기도 하다.

타입을 지운 시퀀스

Swift 표준 라이브러리에는 AnySequence라는 제네릭 구조체도 만들어져 있다.

AnySequence: 타입을 지운(type-erased) 시퀀스 – AnySequence의 인스턴스는 모든 동작을 동일 Element 타입을 가진 내부의 베이스 시퀀스에게 이양하며, 내부 베이스에 관한 정보를 은닉합니다.7

레퍼런스 문서에서 상당히 앞쪽에 나오기 때문에 sequencerepeateElement 혹은 그외의 시퀀스를 생성하는 여러 메소드들과 관련이 있을 것 같지만, 그렇지는 않다.8


  1. Stridable이 아닌 것 같지만 실제로는 맞음. 
  2. Float, Double 등의 실수형 타입도 실제로는 Stridable 프로토콜을 따르고 있다. 실수값들은 실제로 “거의 연속적”이지만 이것이 가능한 이유는 Stridable은 비교가능하며 두 지점 사이의 거리를 구할 수 있기만 하면 되기 때문이다. 
  3. Repeated: Apple Developer 
  4. 그런데 등차수열이나 등비수열을 뭐 얼마나 유용하게 쓰게 될지는 모르겠다만. 
  5. 물론 이 함수는 피보나치 수열만 계산하는데 유용할 것 같다. 
  6. UnfoldSequence, Apple Developer 
  7. AnySequenceApple Developer 
  8. 내부에서 어떤 Boiler plate용으로 쓰이는 것 같음.