공식문서 상에 아래와 같이 정의되어 있습니다.
protocol Sequence<Element>
public protocol Sequence {
/// A type representing the sequence's elements
associatedtype Element where Self.Element == Self.Iterator.Element
/// A type that provides the sequence's iteration interface and
/// encapsulates iths iteration state.
associatedtype Iterator: IteratorProtocol
/// Returns an iterator over the elements of this sequence.
func makeIterator() -> Self.Iterator
}
A type that provides sequential, iterated access to its elements.
= 특정 요소들에 순차적으로 그리고 반복적으로 접근이 가능한 타입
간단하게 생각하면, Array(배열)의 동작들을 생각하면 됩니다. 배열처럼 순서대로 접근이 가능하고, 접근 할때 이터레이터를 사용할 수 있으니까요.
사실 우리가 사용하는 "Array", "Set", "Dictionary" 는 Collection
프로토콜을 준수하고 있습니다. 그리고 그 Collection
프로토콜을 다시, Sequence
프로토콜을 준수하고 있죠.
객체 간 포함관계
Array, Set, Dictionary > Collection(Protocol) > Sequence(Protocol)
정리하면,
Sequence 는 "한 번에 하나의 스탭씩" 진행할 수 있는 값들의 리스트 입니다.
공식문서에서, 배열에 특정 element 가 있는지 확인하는 코드를 예시로 설명합니다.
// 주어진 값
let myBag = ["열쇠", "아이폰", "아아패드", "맥북", "충전기"]
// 결과 값
var hasiPhone = false
// 순회하며 찾는다.
for item in myBag {
if (item == "아이폰") {
hasiPhone = true
break
}
}
print("아이폰 소유여부 : \(hasiPhone)")
// 주어진 값
let myBag = ["열쇠", "아이폰", "아아패드", "맥북", "충전기"]
// 결과값
let hasiPhone = myBag.contain("아이폰")
print("아이폰 소유여부 : \(hasiPhone)")
위 코드나 아래 코드나, 결과에는 차이가 없습니다만, 이미 Sequence 에 내장된 기능들이 많이 있으므로, 해당 기능을 최대한 이용하는게 좋아보입니다.
for element in sequence {
if ... some condition { break }
}
for element in sequence {
// do nothing...
}
위에서 sequence 를 다음과 같이 정의했습니다.
Sequence 는 "한 번에 하나의 스탭씩" 진행할 수 있는 값들의 리스트 입니다.
여기에 "순서대로" 라는 말은 없습니다.
그래서 공식문서에서도 위에 있는 for-in Loop 가 반드시 처음부터 순서대로 마지막 Element 까지 접근한다고 섣불리 판단하면 안된다고 합니다.
왜냐하면, Sequence 프로토콜을 따르도록 구성한 커스텀 구조체가 있다면, 어떻게 흘러갈지 모르기 때문입니다.
Sequence 프로토콜에 정의된 것은 여러 가지가 있습니다. (공식문서)
ex)
func makeIterator() -> Self.iterator
func contains(Self.Elment) -> Bool
func allSatisfy(Self.Element) throws -> Bool) reThows -> Bool
func first(where: (Self.Elemnt) thrwos -> Bool) rethrows -> Self.Element
공식문서에 있는 이러한 여러가지 구현된 함수를 사용하기 위해서는 makeIterator
메소드만 구현해주면 이용할 수 있습니다.
다만, makeIterator
메소드를 구현하려면, IteratorProtocol
타입을 만족해야 구현이 가능합니다.
Custom Sequcen 를 구현하기 전에 그 내부에 있는 햄수인 IteratorProtocol
을 조금 살펴보시죠.
위에 정의에 보면 이부분이 있었죠.
public protocol Sequence {
...
/// A type that provides the sequence's iteration interface and
/// encapsulates iths iteration state.
associatedtype Iterator: IteratorProtocol
/// Returns an iterator over the elements of this sequence.
func makeIterator() -> Self.Iterator
}
A type that supplies the values of a sequence one at a time.
protocol IteratorProtocol<Element>
Swift / Swift Standard Library / Collections / Sequence and Collection / IteratorProtocol
한 문장으로 정리하면 다음과 같습니다.
반복자(Iterator) 를 사용하고 싶으면, 프로토콜을 준수하면 된다.
자세한 설명보다는 커스텀한 코드를 먼저 보여드리겠습니다.
import Foundation
// Sequence 타입을 준수하는 모든 객체는 아래 추가한 객체에 접근할 수 있습니다.
extension Sequence {
/// reduce 메소드를 커스텀하여 구현합니다.
/// 파라미터로 전달받은 함수가 두 값(앞에 값 뒤에 값) 비교조건이 됩니다.
func customReduce(
_ nextPartialResult: (Element, Element) -> Element
) -> Element? {
// 1. 순환을 해야하므로 순환자를 생성합니다.
var i = makeIterator()
// 2. 다음 값이 없다면, 비교가 불가능하므로 nil 인지 확인합니다.
// 3. 해당 값이 있다면, 해당 값을 결과값에 잠시 저장합니다.
guard var accumulated = i.next() else {
return nil
}
// 4. 해당값과 순환값을 파라미터로 받은 조건에 따라 리턴하고, 결과값에 저장합니다.
while let current = i.next() {
accumulated = nextPartialResult(accumulated, current)
}
// 5. 최종 값을 리턴합니다.
return accumulated
}
}
/* 호출 */
let animals = ["Antelope", "Butterfly", "Camel", "Dolphin"]
let longestAnimal = animals.customReduce { current, element in
if current.count > element.count {
return current
} else {
return element
}
}
print(longestAnimal ?? "Nothing...")
// Prints "Butterfly"
다시 돌아와서 Sequence 를 커스텀해보겠습니다.
(예시는 공식문서)
아까 Sequecen는 IteratorProtocol 을 리턴하는 함수를 구현해야합니다. 그래서 총 2 개의 커스텀 객체를 구현하면 됩니다.
struct Countdown: Sequence {
let start: Int
// 커스텀한 순환자를 리턴합니다.
// 이 순환자를 통해서 다음 값이 무엇인지 결정됩니다.
func makeIterator() -> CountdownIterator {
return CountdownIterator(self)
}
}
struct CountdownIterator: IteratorProtocol {
let countdown: Countdown
var times = 0
init(_ countdown: CountDown) {
self.countdown = countdown
}
mutating func next() -> Int? {
let nextNumber = countdown.start - times
guard nextNumber > 0 else {
return nil
}
times += 1
return nextNumber
}
}
let threeTwoOne = Countdown(start: 3)
for count in threeTwoOne {
print("\(count)...")
}
위 코드는, Sequence 따로 구성하고, IteratorProtocol을 또 따로 구성하고 있습니다. 그런데, 사실 하나의 객체가 모두 구현하고, func next() -> Element?
만 구현하더라도 충분합니다.
struct Countdown: Sequence, IteratorProtocol {
let start: Int
var times: Int = 0
mutating func next() -> Int? {
let nextNumber = start - times
guard nextNumber > 0 else { return nil }
times += 1
return nextNumber
}
}
/* 호출 */
let countdown = Countdown(start: 5)
for i in countdown {
print(i)
}
위에 func next() -> Element?
함수는 탈출 조건으로 guard nextNumber > 0 ...
이 있었습니다. 이 조건에 만약 탈출 조건을 주지 않는다면, 무한히 호출되게 됩니다.
// T 로 value 의 타입을 입력받습니다. (제네릭)
struct Repeator<T>: Sequence, IteratorProtocol {
let value: T
// 입력받은 값과 동일한 타입의 값을 리턴합니다.
mutating func next() -> T? {
return value
}
}
// 최초에 5 라는 값 그리고 Type 으로 T 를 입력받습니다.
let repeator = Repeator<Int>(value: 5)
// 무한히 반복되므로 전체 값중에서 앞에 10 개만 가져오고, 해당 값에 대해 모두 print문을 호출합니다.
_ = repeator.prefix(10).map { print($0) }
func next() -> Element?
에서 탈출 조건을 주거나 호출 지점에서 주면 됩니다.Sequence 는 Array 와 유사해보입니다. 다만, Array 처럼 RandomAccess
의 기능은 없습니다. 단어 뜻 그대로 순차적으로 진행하는 행위만 규정하고 있는 타입입니다. 그래서 함수형 프로그래밍에서의 관점에서 보면, 상당히 자주 쓰일 개념입니다. 하나의 Sequence 를 받아서, 해당 Sequence 의 Element 를 각각 어떻게 처리할지 (Operator) 를 결정하고, 마지막으로 구독중인 UI 에 데이터 변경을 전파하니까요. 그래서 그 근간으로 Sequence 라고 생각해도 될 것 같습니다.
물론, Rx 나 Combine 을 보면, 이에 맞게 모두 맵, 필터, 리듀스 등등 다양한 오퍼레이터들이 구현되어 있죠. 다만, Sequence 가 있었고, 그것을 발전 시킨 것이라고 봐도 될 것 같네요.