Swift - Sequence

Uno·2022년 12월 21일
0

Tip-Swift

목록 보기
24/26
post-custom-banner

Sequence

Sequecne 란?

공식문서 상에 아래와 같이 정의되어 있습니다.

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
}
  • Sequecne 는 프로토콜입니다.
  • Element 라는 이름으로 타입을 제네릭을 전달받네요.

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 는 "한 번에 하나의 스탭씩" 진행할 수 있는 값들의 리스트 입니다.

Sequence 기능 예시

공식문서에서, 배열에 특정 element 가 있는지 확인하는 코드를 예시로 설명합니다.

  1. Sequence 내장 기능을 사용하지 않은 경우

// 주어진 값
let myBag = ["열쇠", "아이폰", "아아패드", "맥북", "충전기"]

// 결과 값
var hasiPhone = false

// 순회하며 찾는다.
for item in myBag {
	if (item == "아이폰") {
		hasiPhone = true
		break
	}   
}

print("아이폰 소유여부 : \(hasiPhone)")
  1. Sequence 내장 기능을 사용한 경우
// 주어진 값
let myBag = ["열쇠", "아이폰", "아아패드", "맥북", "충전기"]
// 결과값
let hasiPhone = myBag.contain("아이폰")
print("아이폰 소유여부 : \(hasiPhone)")

위 코드나 아래 코드나, 결과에는 차이가 없습니다만, 이미 Sequence 에 내장된 기능들이 많이 있으므로, 해당 기능을 최대한 이용하는게 좋아보입니다.

Sequence 를 사용할 때, 유의사항

for element in sequence {
	if ... some condition { break }
}

for element in sequence {
	// do nothing...
}

위에서 sequence 를 다음과 같이 정의했습니다.

Sequence 는 "한 번에 하나의 스탭씩" 진행할 수 있는 값들의 리스트 입니다.

여기에 "순서대로" 라는 말은 없습니다.
그래서 공식문서에서도 위에 있는 for-in Loop 가 반드시 처음부터 순서대로 마지막 Element 까지 접근한다고 섣불리 판단하면 안된다고 합니다.

왜냐하면, Sequence 프로토콜을 따르도록 구성한 커스텀 구조체가 있다면, 어떻게 흘러갈지 모르기 때문입니다.

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
  • ... etc

공식문서에 있는 이러한 여러가지 구현된 함수를 사용하기 위해서는 makeIterator 메소드만 구현해주면 이용할 수 있습니다.

다만, makeIterator 메소드를 구현하려면, IteratorProtocol 타입을 만족해야 구현이 가능합니다.

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
}
  • Sequence 가 IteratorProtocol 을 따르는 객체를 사용하고 있습니다. 그 덕분에, 순환하여 접근할 수 있는 거죠.

A type that supplies the values of a sequence one at a time.

protocol IteratorProtocol<Element>
  • 특정 타입으로, 그 타입은 시퀀스의 여러 값들을 한 번에 하나씩 제공해주는 정보를 가진 타입이자 프로토콜 입니다.
  • Document 의 위치는 다음과 같습니다.
    - 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"
  • 이 코드에서 봐야할 부분은 "1. 순환을 해야하므로 순환자를 생성합니다." 부분 입니다.
  • Sequence 타입은 이미 Iterator 를 생성하고 있으므로, 위와 같이 순환자에 바로 접근이 가능합니다.
  • Iterator Protocol 을 따르는 객체는 위와 같이 순환자의 역할을 해줍니다.

다시 돌아와서 Sequence 를 커스텀해보겠습니다.
(예시는 공식문서)

아까 Sequecen는 IteratorProtocol 을 리턴하는 함수를 구현해야합니다. 그래서 총 2 개의 커스텀 객체를 구현하면 됩니다.

  • Custom Sequence
struct Countdown: Sequence {
	let start: Int

	// 커스텀한 순환자를 리턴합니다.
	// 이 순환자를 통해서 다음 값이 무엇인지 결정됩니다.
	func makeIterator() -> CountdownIterator {
		return CountdownIterator(self)
	}
}
  • Custom Iterator
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? 만 구현하더라도 충분합니다.

  • Sequence 와 IteratorProtocol 을 하나의 구조체에서 구현
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 가 있었고, 그것을 발전 시킨 것이라고 봐도 될 것 같네요.

참고자료

profile
iOS & Flutter
post-custom-banner

0개의 댓글