Swift 기본함수 중에는 <a href="https://soooprmx.com/archives/7047">Sequence</a>
를 만드는 함수들이 제법 있다. 이러한 함수 중에서 가장 많이 사용할 법한 함수로 우선 stride()
를 들 수 있다. 이 함수는 파라미터가 다른 두 가지 버전이 있는데 하나는 stride(from:to:by:)
이고 다른 하나는 stride(from:through:by:)
이다. 첫번째 버전은 to:
뒤의 경계를 포함하지 않으며, 두 번째 버전은 ClosedRange
처럼 뒤쪽 경계값을 포함한다.
이 함수들은 주로 for 문과 같이 쓰이면서 주어진 범위 내에서 step
을 달리하여 건너뛰는 값들을 하나씩 얻을 수 있게 한다. 보통 정수범위의 이터레이션에 쓰이는데, 다음과 같이 10~20사이에서 0.2 씩 뛰면서 반복할 수도 있다.
for a in stride(from:10.0, to:20.0, by:0.2) { print(a) }
혹은 거꾸로 내려가는 값들을 표현하기에도 좋다.
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:)
다음으로는 repeatElement(_:count:)
가 있다. 이는 특정 원소가 지정된 개수만큼 반복되는 시퀀스를 생성한다. 이 때의 리턴타입은 제네릭 구조체인 Repeated
이며 이는 Collection
의 일종이다.
Repeated : 모든 원소가 동일한 콜렉션3
sequence()
다음은 이름 그대로 시퀀스를 만들 것 처럼 생긴 sequence()
함수인데 여기에도 두 가지 버전이 존재한다. 그 중 하나는 sequence(first:next:)
인데 시작값과, 어떤 값으로부터 다음 값을 만드는 함수를 이용해서 무한 수열을 생성할 수 있다. (이는 시퀀스의 원리상 정의와 비슷하다. 첫 원소와, 특정 원소에서의 다음원소를 계산할 수 있으면 정해진 길이만큼의 수열을 순차적-sequential-으로 구할 수 있다.) 어떤 원소로부터 그 다음 원소는 현재 원소 값을 기준으로 만들 수 있으므로, 등차수열이나 등비수열을 만들 때 유용하게 쓸 수 있다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
두 번째 sequence함수는 sequence(state:)
로 앞의 함수와 비슷하게 생겼는데, 값이 아닌 계산에 필요한 팩터들을 하나의 상태에 저장해두고 이를 계속 변경하면서 다음항을 계산할 수 있다. 일례로 피보나치 수열을 다음과 같이 계산할 수 있다. 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
프로퍼티를 이용하는 것이다. 아래 코드는 비슷하게 생겼지만, for 문을 돌면서 매 원소가 계산된다. (만약 for 루프 내에서 break 등으로 루프를 탈출한다면
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
레퍼런스 문서에서 상당히 앞쪽에 나오기 때문에 sequence
나 repeateElement
혹은 그외의 시퀀스를 생성하는 여러 메소드들과 관련이 있을 것 같지만, 그렇지는 않다.8
Stridable
이 아닌 것 같지만 실제로는 맞음. ↩Float
,Double
등의 실수형 타입도 실제로는Stridable
프로토콜을 따르고 있다. 실수값들은 실제로 “거의 연속적”이지만 이것이 가능한 이유는Stridable
은 비교가능하며 두 지점 사이의 거리를 구할 수 있기만 하면 되기 때문이다. ↩Repeated
: Apple Developer ↩- 그런데 등차수열이나 등비수열을 뭐 얼마나 유용하게 쓰게 될지는 모르겠다만. ↩
- 물론 이 함수는 피보나치 수열만 계산하는데 유용할 것 같다. ↩
UnfoldSequence
, Apple Developer ↩ ↩AnySequence
, Apple Developer ↩- 내부에서 어떤 Boiler plate용으로 쓰이는 것 같음. ↩